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



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



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



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



Я исследовал как pytables, так и h5py, и до сих пор не видел пользы от них для своей цели.

1437   1  

1 ответ:

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

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

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

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

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

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

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

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

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

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


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

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

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

тем не менее, будущее numpy-подобных вне ядра расчеты-это Блейз. посмотреть если вы действительно хотите пойти по этому пути.


случай "unchunked"

во-первых, рассмотрим 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 Гб) 3D-массива, хранящегося таким образом и читать через memmap, чтение полного среза вдоль" худшей " оси может легко занять десятки минут, даже с современным оборудованием. В то же время, срез вдоль лучшей оси может занять меньше секунды. Для простоты я показываю только" полные " срезы вдоль одной оси, но то же самое происходит с произвольными срезами любого подмножества данных.

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


простой "chunked" case

допустим, мы храним 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 запросов для версии без фрагментов. (Но в лучшем случае все еще 6 ищет против 1 для массива memmapped.) Поскольку последовательное чтение выполняется очень быстро по сравнению с поиском, это значительно сокращает время, необходимое для чтения произвольного подмножества в память. Еще раз, этот эффект становится больше с большими массивами.

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


куски массивов с 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 ГБ), упорядоченного по Фортрану 3D-массива, содержащего те же самые точные данные.

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

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

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

The 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-ordered:" 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 массивом вы застряли с этим несоответствием ввода/вывода (или, возможно, анизотропия-лучший термин). Однако с помощью фрагментированного набора данных HDF можно выбрать размер фрагмента таким образом, чтобы доступ был либо равен, либо оптимизирован для конкретного случая использования. Это дает вам гораздо больше гибкости.

в резюме

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

в целом, попробовать его и посмотреть, если это работает для вашего случая. Думаю, вы будете удивлены.

Comments

    Ничего не найдено.