Подтвердить что ты не робот

Есть ли скорость анализа или использование памяти для использования HDF5 для хранения больших массивов (вместо плоских двоичных файлов)?

Я обрабатываю большие 3D-массивы, которые мне часто нужно нарезать различными способами, чтобы делать разнообразный анализ данных. Типичный "куб" может быть ~ 100 ГБ (и, вероятно, в будущем будет больше)

Похоже, что типичный рекомендуемый формат файла для больших наборов данных в python - использовать HDF5 (либо h5py, либо pytables). Мой вопрос: есть ли какая-либо скорость или использование памяти для использования HDF5 для хранения и анализа этих кубов за их хранение в простых плоских двоичных файлах? Является ли HDF5 более подходящим для табличных данных, в отличие от больших массивов, как то, с чем я работаю? Я вижу, что HDF5 может обеспечить хорошее сжатие, но меня больше интересует скорость обработки и обработка переполнения памяти.

Я часто хочу проанализировать только одно большое подмножество куба. Один из недостатков обоих pytables и h5py заключается в том, что когда я беру кусочек массива, я всегда возвращаю массив numpy, используя память. Однако, если я нарезаю numpy memmap плоского двоичного файла, я могу получить представление, которое хранит данные на диске. Таким образом, кажется, что я могу более легко анализировать определенные разделы моих данных, не перегружая свою память.

Я изучил как pytables, так и h5py, и не видел преимуществ до сих пор для моей цели.

4b9b3361

Ответ 1

HDF5 Преимущества: Организация, гибкость, интероперабельность

Некоторые из основных преимуществ HDF5 - его иерархическая структура (аналогичная папкам/файлам), необязательные произвольные метаданные, хранящиеся в каждом элементе, и его гибкость (например, сжатие). Эта организационная структура и хранилище метаданных могут казаться тривиальными, но это очень полезно на практике.

Другим преимуществом HDF является то, что наборы данных могут быть как фиксированного, так и гибкого размера. Таким образом, легко добавлять данные в большой набор данных без необходимости создавать целую новую копию.

Кроме того, HDF5 является стандартизованным форматом с библиотеками, доступными практически для любого языка, поэтому совместное использование данных на диске между, скажем, Matlab, Fortran, R, C и Python очень просто с HDF. (Чтобы быть справедливым, это не слишком сложно с большим двоичным массивом, если вы знаете порядок C и F и знаете форму, dtype и т.д. Сохраненного массива.)

Преимущества HDF для большого массива: более быстрый ввод/вывод произвольного среза

Точно так же, как TL/DR:. Для 3D-массива размером ~ 8 ГБ чтение "полного" среза вдоль любой оси заняло ~ 20 секунд с набором данных HDF5 и 0,3 секунды (в лучшем случае ) до более чем трех часов (наихудший случай) для memmapped массива одних и тех же данных.

Помимо вышеперечисленных вещей, существует еще одно большое преимущество в формате "chunked" * on-disk data, таком как HDF5: чтение произвольного фрагмента (выделение на произвольное), как правило, будет намного быстрее, поскольку данные на диске более сопредельных в среднем.

* (HDF5 не обязательно должен быть записанным форматом данных. Он поддерживает chunking, но не требует его. Фактически, значение по умолчанию для создания набора данных в h5py не является фрагментом, если я правильно помните.)

В принципе, ваша максимальная скорость чтения на диске и скорость чтения диска с наименьшим регистром для данного фрагмента вашего набора данных будут достаточно близки к набору данных HDF (если вы выбрали разумный размер фрагмента или позвольте библиотеке выбрать один для вы). С простым двоичным массивом наилучший вариант быстрее, но худший вариант намного хуже.

Одно предупреждение, если у вас SSD, вы, вероятно, не заметите огромной разницы в скорости чтения/записи. Однако с помощью обычного жесткого диска последовательные чтения намного, намного быстрее, чем случайные чтения. (т.е. обычный жесткий диск имеет длительное время seek). HDF по-прежнему имеет преимущество на SSD, но больше благодаря другим функциям (например, метаданные, организации и т.д.), чем из-за необработанной скорости.


Прежде всего, чтобы устранить путаницу, доступ к набору данных h5py возвращает объект, который ведет себя аналогично массиву numpy, но не загружает данные в память, пока не нарезается. (Подобно memmap, но не идентичны.) Для получения дополнительной информации см. h5py.

Нарезка набора данных будет загружать подмножество данных в память, но, предположительно, вы хотите что-то с ней сделать, и в этом случае вам все равно понадобится это в памяти.

Если вы хотите выполнять внекорпоративные вычисления, вы можете довольно легко использовать табличные данные с помощью pandas или pytables. Это возможно при h5py (лучше для больших массивов N-D), но вам нужно опуститься на нижний уровень касания и самостоятельно обработать итерацию.

Тем не менее, будущее многопользовательских вычислений - Blaze. Посмотрите на него, если вы действительно хотите пройти этот маршрут.


"unchunked" case

Сначала рассмотрим 3D-C-упорядоченный массив, записанный на диск (я смоделирую его, вызвав arr.ravel() и распечатав результат, чтобы сделать вещи более заметными):

In [1]: import numpy as np

In [2]: arr = np.arange(4*6*6).reshape(4,6,6)

In [3]: arr
Out[3]:
array([[[  0,   1,   2,   3,   4,   5],
        [  6,   7,   8,   9,  10,  11],
        [ 12,  13,  14,  15,  16,  17],
        [ 18,  19,  20,  21,  22,  23],
        [ 24,  25,  26,  27,  28,  29],
        [ 30,  31,  32,  33,  34,  35]],

       [[ 36,  37,  38,  39,  40,  41],
        [ 42,  43,  44,  45,  46,  47],
        [ 48,  49,  50,  51,  52,  53],
        [ 54,  55,  56,  57,  58,  59],
        [ 60,  61,  62,  63,  64,  65],
        [ 66,  67,  68,  69,  70,  71]],

       [[ 72,  73,  74,  75,  76,  77],
        [ 78,  79,  80,  81,  82,  83],
        [ 84,  85,  86,  87,  88,  89],
        [ 90,  91,  92,  93,  94,  95],
        [ 96,  97,  98,  99, 100, 101],
        [102, 103, 104, 105, 106, 107]],

       [[108, 109, 110, 111, 112, 113],
        [114, 115, 116, 117, 118, 119],
        [120, 121, 122, 123, 124, 125],
        [126, 127, 128, 129, 130, 131],
        [132, 133, 134, 135, 136, 137],
        [138, 139, 140, 141, 142, 143]]])

Значения будут сохранены на диске последовательно, как показано в строке 4 ниже. (Пусть игнорирует данные файловой системы и фрагментацию на данный момент.)

In [4]: arr.ravel(order='C')
Out[4]:
array([  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,
        13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,
        26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,
        39,  40,  41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,
        52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  62,  63,  64,
        65,  66,  67,  68,  69,  70,  71,  72,  73,  74,  75,  76,  77,
        78,  79,  80,  81,  82,  83,  84,  85,  86,  87,  88,  89,  90,
        91,  92,  93,  94,  95,  96,  97,  98,  99, 100, 101, 102, 103,
       104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
       117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
       130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])

В лучшем случае давайте возьмем срез вдоль первой оси. Обратите внимание, что это только первые 36 значений массива. Это будет очень быстро прочитано! (один искать, один читать)

In [5]: arr[0,:,:]
Out[5]:
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23],
       [24, 25, 26, 27, 28, 29],
       [30, 31, 32, 33, 34, 35]])

Аналогично, следующий срез вдоль первой оси будет всего лишь следующими 36 значениями. Чтобы прочитать полный срез вдоль этой оси, нам нужна только одна операция seek. Если все, что мы собираемся читать, это разные фрагменты вдоль этой оси, то это идеальная файловая структура.

Однако рассмотрим наихудший сценарий: срез вдоль последней оси.

In [6]: arr[:,:,0]
Out[6]:
array([[  0,   6,  12,  18,  24,  30],
       [ 36,  42,  48,  54,  60,  66],
       [ 72,  78,  84,  90,  96, 102],
       [108, 114, 120, 126, 132, 138]])

Чтобы прочитать этот фрагмент, нам нужно 36 запросов и 36 просмотров, так как все значения разделены на диске. Ни одна из них не смежна!

Это может показаться довольно незначительным, но по мере того как мы получаем большие и большие массивы, количество и размер операций seek быстро растут. Для трехмерного массива большого размера (~ 10 Гб), сохраненного таким образом и считанного через memmap, чтение полного фрагмента вдоль "худшей" оси может легко занять десятки минут даже при использовании современного оборудования. В то же время срез вдоль наилучшей оси может занимать менее секунды. Для простоты я показываю только "полные" фрагменты вдоль одной оси, но то же самое происходит с произвольными срезами любого подмножества данных.

Кстати, есть несколько форматов файлов, которые используют это и в основном хранят на диске три копии огромных 3D-массивов на диске: один в C-порядке, один в F-порядке и один в промежуточном между ними. (Примером этого является формат Geoprobe D3D, хотя я не уверен, что он где-то документирован.) Кому все равно, если размер финального файла составляет 4 ТБ, хранилище дешево! Сумасшедшая вещь в том, что, поскольку основной вариант использования - извлечение одного суб-среза в каждом направлении, чтение, которое вы хотите сделать, очень и очень быстро. Он работает очень хорошо!


Простой "случайный" случай

Скажем, мы храним 2x2x2 "куски" 3D-массива в виде смежных блоков на диске. Другими словами, что-то вроде:

nx, ny, nz = arr.shape
slices = []
for i in range(0, nx, 2):
    for j in range(0, ny, 2):
        for k in range(0, nz, 2):
            slices.append((slice(i, i+2), slice(j, j+2), slice(k, k+2)))

chunked = np.hstack([arr[chunk].ravel() for chunk in slices])

Таким образом, данные на диске будут выглядеть как chunked:

array([  0,   1,   6,   7,  36,  37,  42,  43,   2,   3,   8,   9,  38,
        39,  44,  45,   4,   5,  10,  11,  40,  41,  46,  47,  12,  13,
        18,  19,  48,  49,  54,  55,  14,  15,  20,  21,  50,  51,  56,
        57,  16,  17,  22,  23,  52,  53,  58,  59,  24,  25,  30,  31,
        60,  61,  66,  67,  26,  27,  32,  33,  62,  63,  68,  69,  28,
        29,  34,  35,  64,  65,  70,  71,  72,  73,  78,  79, 108, 109,
       114, 115,  74,  75,  80,  81, 110, 111, 116, 117,  76,  77,  82,
        83, 112, 113, 118, 119,  84,  85,  90,  91, 120, 121, 126, 127,
        86,  87,  92,  93, 122, 123, 128, 129,  88,  89,  94,  95, 124,
       125, 130, 131,  96,  97, 102, 103, 132, 133, 138, 139,  98,  99,
       104, 105, 134, 135, 140, 141, 100, 101, 106, 107, 136, 137, 142, 143])

И просто чтобы показать, что они являются блоками 2x2x2 arr, обратите внимание, что это первые 8 значений chunked:

In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0,  1],
        [ 6,  7]],

       [[36, 37],
        [42, 43]]])

Чтобы читать в любом фрагменте вдоль оси, мы читали либо 6 или 9 смежных фрагментов (в два раза больше данных, чем нам нужно), а затем сохраняли только ту часть, которую мы хотели. Это наихудший максимум из 9 просмотров против максимум 36 запросов для не-chunked версии. (Но лучший вариант - 6 просмотров vs 1 для memmapped array.) Поскольку последовательные чтения очень быстрые по сравнению с запросами, это значительно сокращает время, затрачиваемое на чтение произвольного подмножества в память. И снова этот эффект становится больше при больших массивах.

HDF5 делает это на несколько шагов дальше. Куски не должны храниться смежно, и они индексируются B-Tree. Кроме того, они не должны быть одного размера на диске, поэтому сжатие может быть применено к каждому фрагменту.


Разбитые массивы с h5py

По умолчанию, h5py не создает фрагментированные HDF файлы на диске (я думаю, pytables делает, наоборот). Однако, если вы укажете chunks=True при создании набора данных, на диске вы получите разбитый массив.

Как быстрый, минимальный пример:

import numpy as np
import h5py

data = np.random.random((100, 100, 100))

with h5py.File('test.hdf', 'w') as outfile:
    dset = outfile.create_dataset('a_descriptive_name', data=data, chunks=True)
    dset.attrs['some key'] = 'Did you want some metadata?'

Обратите внимание, что chunks=True сообщает h5py автоматически выбирать размер блока для нас. Если вы знаете больше о своем наиболее распространенном прецеденте, вы можете оптимизировать размер/форму куска, указав кортеж формы (например, (2,2,2) в простом примере выше). Это позволяет сделать чтение по определенной оси более эффективным или оптимизировать для чтения/записи определенного размера.


Сравнение производительности ввода/вывода

Просто чтобы подчеркнуть суть, давайте сравним чтение в срезах из набора данных HDF5 и большой (~ 8 ГБ), Fortran-упорядоченный 3D-массив, содержащий те же точные данные.

очистил все кэши OS между каждым прогоном, поэтому мы видим "холодную" производительность.

Для каждого типа файла мы проверим чтение в "полном" x-срезе вдоль первой оси и "полный" z-slize вдоль последней оси. Для упорядоченного по Fortran memmapped массиву "x" срез является наихудшим случаем, а срез "z" - лучший случай.

Используемый код в сущности (включая создание файла hdf). Я не могу легко использовать данные, используемые здесь, но вы могли бы имитировать массив с нулями нужной формы (621, 4991, 2600) и type np.uint8.

chunked_hdf.py выглядит следующим образом:

import sys
import h5py

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    f = h5py.File('/tmp/test.hdf5', 'r')
    return f['seismic_volume']

def z_slice(data):
    return data[:,:,0]

def x_slice(data):
    return data[0,:,:]

main()

memmapped_array.py похож, но имеет большую сложность, чтобы обеспечить загрузку фрагментов в память (по умолчанию будет возвращен массив memmapped, который не будет сравнивать яблоки с яблоками).

import numpy as np
import sys

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    big_binary_filename = '/data/nankai/data/Volumes/kumdep01_flipY.3dv.vol'
    shape = 621, 4991, 2600
    header_len = 3072

    data = np.memmap(filename=big_binary_filename, mode='r', offset=header_len,
                     order='F', shape=shape, dtype=np.uint8)
    return data

def z_slice(data):
    dat = np.empty(data.shape[:2], dtype=data.dtype)
    dat[:] = data[:,:,0]
    return dat

def x_slice(data):
    dat = np.empty(data.shape[1:], dtype=data.dtype)
    dat[:] = data[0,:,:]
    return dat

main()

Посмотрим сначала на производительность HDF:

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py z
python chunked_hdf.py z  0.64s user 0.28s system 3% cpu 23.800 total

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py x
python chunked_hdf.py x  0.12s user 0.30s system 1% cpu 21.856 total

"Полный" x-срез и "полный" z-срез занимают примерно такое же количество времени (~ 20 секунд). Учитывая, что это массив размером 8 ГБ, это не так уж плохо. Большую часть времени

И если мы сравним это с временами memmapped массивов (это Fortran-упорядочено: "z-slice" - лучший случай, а "x-slice" - худший случай.):

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py z
python memmapped_array.py z  0.07s user 0.04s system 28% cpu 0.385 total

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py x
python memmapped_array.py x  2.46s user 37.24s system 0% cpu 3:35:26.85 total

Да, вы это правильно прочитали. 0,3 секунды для одного направления среза и ~ 3,5 часа для другого.

Время среза в направлении "x" намного длиннее времени, необходимого для загрузки всего массива 8 ГБ в память и выбора фрагмента, который мы хотели! (Опять же, это упорядоченный по Фортрану массив. Простой момент времени среза x/z будет иметь место для C-упорядоченного массива.)

Однако, если мы всегда хотим взять срез в лучшем случае, большой двоичный массив на диске очень хорош. (~ 0,3 с!)

С memmapped array вы застряли в этом несоответствии ввода/вывода (или, возможно, анизотропия - лучший термин). Однако, с набором данных HDF, вы можете выбрать chunksize, чтобы доступ был либо равным, либо оптимизирован для конкретного прецедента. Это дает вам большую гибкость.

В заключение

Надеюсь, это поможет, во всяком случае, решить одну часть вашего вопроса. HDF5 имеет много других преимуществ по сравнению с "сырыми" memmaps, но у меня нет возможности расширять их все здесь. Сжатие может ускорить некоторые вещи (данные, с которыми я работаю, не сильно выигрывают от сжатия, поэтому я редко их использую), а кэширование на уровне ОС часто играет лучше с файлами HDF5, чем с "сырыми" mem-картами. Кроме того, HDF5 - действительно фантастический формат контейнера. Это дает вам большую гибкость в управлении вашими данными и может использоваться из более или менее любого языка программирования.

В целом, попробуйте и посмотрите, хорошо ли это подходит для вашего случая использования. Я думаю, вы можете быть удивлены.