Повышение производительности записи таблиц HDF5 для панд (PyTables?)
В течение двух месяцев мы использовали панд для исследований, что дало отличный эффект. С большим количеством наборов данных трассировки среднего размера pandas + PyTables (интерфейс HDF5) выполняет огромную работу, позволяя мне обрабатывать разнородные данные, используя все инструменты Python, которые я знаю и люблю.
Вообще говоря, я использую Fixed (ранее)Шторер») в PyTables, так как мой рабочий процесс выполняется с однократной записью и чтением, и многие из моих наборов данных имеют такой размер, что я могу загружать 50-100 из них в память за раз без серьезных недостатков. (Примечание: большую часть своей работы я выполняю на машинах серверного класса Opteron с объемом системной памяти более 128 ГБ.)
Однако для больших наборов данных (500 МБ и более) я хотел бы иметь возможность использовать более масштабируемые возможности произвольного доступа и запросов в PyTables ».Таблицы» формат, чтобы я мог выполнять свои запросы вне памяти, а затем загружать гораздо меньший набор результатов в память для обработки. Однако большим препятствием здесь является производительность записи. Да, как я уже сказал, мой рабочий процесс - однократная запись, много читается, но относительное время все еще неприемлемо.
Например, недавно я выполнил большую факторизацию Холецкого, которая заняла 3 минуты 8 секунд (188 секунд) на моем 48-ядерном компьютере. При этом генерируется файл трассировки ~ 2,2 ГБ - трассировка генерируется параллельно с программой, поэтому никаких дополнительных "время создания трассировки. "
Первоначальное преобразование моего двоичного файла трассировки в формат pandas / PyTables занимает приличную часть времени, но в основном потому, что двоичный формат намеренно вышел из строя, чтобы уменьшить влияние на производительность самого генератора трассировки. Это также не имеет отношения к потере производительности при переходе из формата Storer в формат Table.
Мои тесты изначально выполнялись с пандами 0.12, numpy 1.7.1, PyTables 2.4.0 и Numberxpr 0.20.1. Моя 48-ядерная машина работает на частоте 2,8 ГГц на ядро, и я пишу в файловую систему ext3, которая, вероятно, (но не обязательно) на SSD.
Я могу записать весь набор данных в файл HDF5 формата Storer (размер файла: 3,3 ГБ) за 7,1 секунды. Тот же набор данных, записанный в формате таблицы (итоговый размер файла также равен 3,3 ГБ), занимает 178,7 секунды для записи.
Код выглядит следующим образом:
with Timer() as t:
store = pd.HDFStore('test_storer.h5', 'w')
store.put('events', events_dataset, table=False, append=False)
print('Fixed format write took ' + str(t.interval))
with Timer() as t:
store = pd.HDFStore('test_table.h5', 'w')
store.put('events', events_dataset, table=True, append=False)
print('Table format write took ' + str(t.interval))
и выход просто
Fixed format write took 7.1
Table format write took 178.7
Мой набор данных содержит 28 880 943 строки, и столбцы являются основными типами данных:
node_id int64
thread_id int64
handle_id int64
type int64
begin int64
end int64
duration int64
flags int64
unique_id int64
id int64
DSTL_LS_FULL float64
L2_DMISS float64
L3_MISS float64
kernel_type float64
dtype: object
... так что я неЯ думаю, что со скоростью записи должны быть какие-то проблемы с данными.
Мы также пытались добавить сжатие BLOSC, чтобы исключить любые странные проблемы ввода-вывода, которые могут повлиять на один или другой сценарий, но сжатие, похоже, в равной степени снижает производительность обоих.
Теперь я понимаю, что в документации Pandas говорится, что формат Storer предлагает значительно более быструю запись и немного более быстрое чтение. (У меня есть более быстрое чтение, поскольку чтение формата Storer, кажется, занимает около 2,5 секунд, в то время как чтение формата таблицы занимает около 10 секунд.) Но действительно кажется чрезмерным, что запись в формат таблицы должна занимать 25 раз Пока формат Storer напишу.
Может ли кто-нибудь из людей, связанных с PyTables или пандами, объяснить архитектурные (или иные) причины, почему запись в запрашиваемый формат (который явно требует очень мало дополнительных данных) должна занимать на порядок больше времени? И есть ли надежда на улучшение этого в будущем? Я'Мне бы хотелось принять участие в одном или другом проекте, поскольку моя область - это высокопроизводительные вычисления, и я вижу значительный пример использования обоих проектов в этой области .... но было бы полезно получить некоторые разъяснения по этим вопросам. в первую очередь, и / или несколько советов о том, как ускорить процесс от тех, кто знает, как строится система.
РЕДАКТИРОВАТЬ:
Запуск предыдущих тестов с% prun в IPython дает следующий (несколько уменьшенный для удобства чтения) вывод профиля для формата Storer / Fixed:
%prun -l 20 profile.events.to_hdf('test.h5', 'events', table=False, append=False)
3223 function calls (3222 primitive calls) in 7.385 seconds
Ordered by: internal time
List reduced from 208 to 20 due to restriction
ncalls tottime percall cumtime percall filename:lineno(function)
6 7.127 1.188 7.128 1.188 {method '_createArray' of 'tables.hdf5Extension.Array' objects}
1 0.242 0.242 0.242 0.242 {method '_closeFile' of 'tables.hdf5Extension.File' objects}
1 0.003 0.003 0.003 0.003 {method '_g_new' of 'tables.hdf5Extension.File' objects}
46 0.001 0.000 0.001 0.000 {method 'reduce' of 'numpy.ufunc' objects}
и следующее для формата таблиц:
%prun -l 40 profile.events.to_hdf('test.h5', 'events', table=True, append=False, chunksize=1000000)
499082 function calls (499040 primitive calls) in 188.981 seconds
Ordered by: internal time
List reduced from 526 to 40 due to restriction
ncalls tottime percall cumtime percall filename:lineno(function)
29 92.018 3.173 92.018 3.173 {pandas.lib.create_hdf_rows_2d}
640 20.987 0.033 20.987 0.033 {method '_append' of 'tables.hdf5Extension.Array' objects}
29 19.256 0.664 19.256 0.664 {method '_append_records' of 'tables.tableExtension.Table' objects}
406 19.182 0.047 19.182 0.047 {method '_g_writeSlice' of 'tables.hdf5Extension.Array' objects}
14244 10.646 0.001 10.646 0.001 {method '_g_readSlice' of 'tables.hdf5Extension.Array' objects}
472 10.359 0.022 10.359 0.022 {method 'copy' of 'numpy.ndarray' objects}
80 3.409 0.043 3.409 0.043 {tables.indexesExtension.keysort}
2 3.023 1.512 3.023 1.512 common.py:134(_isnull_ndarraylike)
41 2.489 0.061 2.533 0.062 {method '_fillCol' of 'tables.tableExtension.Row' objects}
87 2.401 0.028 2.401 0.028 {method 'astype' of 'numpy.ndarray' objects}
30 1.880 0.063 1.880 0.063 {method '_g_flush' of 'tables.hdf5Extension.Leaf' objects}
282 0.824 0.003 0.824 0.003 {method 'reduce' of 'numpy.ufunc' objects}
41 0.537 0.013 0.668 0.016 index.py:607(final_idx32)
14490 0.385 0.000 0.712 0.000 array.py:342(_interpret_indexing)
39 0.279 0.007 19.635 0.503 index.py:1219(reorder_slice)
2 0.256 0.128 10.063 5.031 index.py:1099(get_neworder)
1 0.090 0.090 119.392 119.392 pytables.py:3016(write_data)
57842 0.087 0.000 0.087 0.000 {numpy.core.multiarray.empty}
28570 0.062 0.000 0.107 0.000 utils.py:42(is_idx)
14164 0.062 0.000 7.181 0.001 array.py:711(_readSlice)
РЕДАКТИРОВАТЬ 2:
Снова запустив предварительную версию pandas 0.13 (выпущенную 20 ноября 2013 г. около 11:00 EST), время записи для формата таблиц значительно улучшилось, но все равнот сравнитьразумно» на скорости записи в формате Storer / Fixed.
%prun -l 40 profile.events.to_hdf('test.h5', 'events', table=True, append=False, chunksize=1000000)
499748 function calls (499720 primitive calls) in 117.187 seconds
Ordered by: internal time
List reduced from 539 to 20 due to restriction
ncalls tottime percall cumtime percall filename:lineno(function)
640 22.010 0.034 22.010 0.034 {method '_append' of 'tables.hdf5Extension.Array' objects}
29 20.782 0.717 20.782 0.717 {method '_append_records' of 'tables.tableExtension.Table' objects}
406 19.248 0.047 19.248 0.047 {method '_g_writeSlice' of 'tables.hdf5Extension.Array' objects}
14244 10.685 0.001 10.685 0.001 {method '_g_readSlice' of 'tables.hdf5Extension.Array' objects}
472 10.439 0.022 10.439 0.022 {method 'copy' of 'numpy.ndarray' objects}
30 7.356 0.245 7.356 0.245 {method '_g_flush' of 'tables.hdf5Extension.Leaf' objects}
29 7.161 0.247 37.609 1.297 pytables.py:3498(write_data_chunk)
2 3.888 1.944 3.888 1.944 common.py:197(_isnull_ndarraylike)
80 3.581 0.045 3.581 0.045 {tables.indexesExtension.keysort}
41 3.248 0.079 3.294 0.080 {method '_fillCol' of 'tables.tableExtension.Row' objects}
34 2.744 0.081 2.744 0.081 {method 'ravel' of 'numpy.ndarray' objects}
115 2.591 0.023 2.591 0.023 {method 'astype' of 'numpy.ndarray' objects}
270 0.875 0.003 0.875 0.003 {method 'reduce' of 'numpy.ufunc' objects}
41 0.560 0.014 0.732 0.018 index.py:607(final_idx32)
14490 0.387 0.000 0.712 0.000 array.py:342(_interpret_indexing)
39 0.303 0.008 19.617 0.503 index.py:1219(reorder_slice)
2 0.288 0.144 10.299 5.149 index.py:1099(get_neworder)
57871 0.087 0.000 0.087 0.000 {numpy.core.multiarray.empty}
1 0.084 0.084 45.266 45.266 pytables.py:3424(write_data)
1 0.080 0.080 55.542 55.542 pytables.py:3385(write)
Во время выполнения этих тестов я заметил, что существуют длительные периоды, когда письмо кажется "Пауза" (файл на диске активно не растет), но в некоторые из этих периодов также используется процессор.
Я начинаю подозревать, что некоторые известные ограничения ext3 могут плохо взаимодействовать с пандами или PyTables. Ext3 и другие файловые системы, не основанные на экстентах, иногда пытаются быстро отсоединить большие файлы, и похожая производительность системы (низкая загрузка ЦП, но длительное время ожидания) очевидна даже во время простого 'гт» 1 ГБ файла, например.
Чтобы уточнить, в каждом тестовом случае я обязательно удалил существующий файл, если таковой имеется, перед началом теста, чтобы не подвергаться каким-либо штрафам за удаление / перезапись файла ext3.
Однако при повторном запуске этого теста с индексом None производительность резко улучшается (~ 50 с против ~ 120 при индексации). Таким образом, может показаться, что любой из этих процессов по-прежнему связан с процессором (моя система имеет относительно старые процессоры AMD Opteron Istanbul, работающие на частоте 2,8 ГГц, хотя у нее также есть 8 разъемов с 6 ядрами в каждом, все из которых, кроме одного, из конечно, сидеть без дела во время записи), или что существует некоторый конфликт между тем, как PyTables или панды пытаются манипулировать / читать / анализировать файл, когда он уже частично или полностью находится в файловой системе, что вызывает патологически плохое поведение ввода-вывода, когда индексация происходит.
РЕДАКТИРОВАТЬ 3:
@Джефф'Предлагаемые тесты на меньшем наборе данных (1,3 ГБ на диске) после обновления PyTables с 2.4 до 3.0.0 привели меня сюда:
In [7]: %timeit f(df)
1 loops, best of 3: 3.7 s per loop
In [8]: %timeit f2(df) # where chunksize= 2 000 000
1 loops, best of 3: 13.8 s per loop
In [9]: %timeit f3(df) # where chunksize= 2 000 000
1 loops, best of 3: 43.4 s per loop
Фактически, моя производительность, кажется, превосходит его во всех сценариях, кроме случаев, когда индексация включена (по умолчанию). Тем не менее, индексация все еще кажется убийцей, и, если ям интерпретировать вывод изtop
а такжеls
поскольку я запускаю эти тесты правильно, остаются периоды времени, когда не происходит ни значительной обработки, ни записи файлов (т. е. загрузка ЦП для процесса Python близка к 0, а размер файла остается постоянным). Я могу только предположить, что это чтение файла. Мне трудно понять, почему чтение файлов вызывает замедление, поскольку я могу надежно загрузить весь файл объемом 3 ГБ с этого диска в память менее чем за 3 секунды. Если они''не файл читает, то что такое система'ждет» на? (Никто больше не вошел в систему, и нет никакой другой активности файловой системы.)
На данный момент, с обновленными версиями соответствующих модулей Python производительность моего исходного набора данных снижается до следующих цифр. Особый интерес представляют системное время, которое, как я полагаю, является, по крайней мере, верхним пределом времени, затрачиваемого на выполнение ввода-вывода, и время стены, которое, возможно, объясняет эти таинственные периоды отсутствия записи / отсутствия активности процессора.
In [28]: %time f(profile.events)
CPU times: user 0 ns, sys: 7.16 s, total: 7.16 s
Wall time: 7.51 s
In [29]: %time f2(profile.events)
CPU times: user 18.7 s, sys: 14 s, total: 32.7 s
Wall time: 47.2 s
In [31]: %time f3(profile.events)
CPU times: user 1min 18s, sys: 14.4 s, total: 1min 32s
Wall time: 2min 5s
Тем не менее, похоже, что индексирование вызывает значительное замедление для моего варианта использования. Возможно, мне следует попытаться ограничить индексированные поля, а не просто выполнять регистр по умолчанию (который вполне может быть индексацией для всех полей в DataFrame)? Я не уверен, как это может повлиять на время запроса, особенно в тех случаях, когда запрос выбирается на основе неиндексированного поля.
Пер Джеффs запрос, ptdump полученного файла.
ptdump -av test.h5
/ (RootGroup) ''
/._v_attrs (AttributeSet), 4 attributes:
[CLASS := 'GROUP',
PYTABLES_FORMAT_VERSION := '2.1',
TITLE := '',
VERSION := '1.0']
/df (Group) ''
/df._v_attrs (AttributeSet), 14 attributes:
[CLASS := 'GROUP',
TITLE := '',
VERSION := '1.0',
data_columns := [],
encoding := None,
index_cols := [(0, 'index')],
info := {1: {'type': 'Index', 'names': [None]}, 'index': {}},
levels := 1,
nan_rep := 'nan',
non_index_axes :=
[(1, ['node_id', 'thread_id', 'handle_id', 'type', 'begin', 'end', 'duration', 'flags', 'unique_id', 'id', 'DSTL_LS_FULL', 'L2_DMISS', 'L3_MISS', 'kernel_type'])],
pandas_type := 'frame_table',
pandas_version := '0.10.1',
table_type := 'appendable_frame',
values_cols := ['values_block_0', 'values_block_1']]
/df/table (Table(28880943,)) ''
description := {
"index": Int64Col(shape=(), dflt=0, pos=0),
"values_block_0": Int64Col(shape=(10,), dflt=0, pos=1),
"values_block_1": Float64Col(shape=(4,), dflt=0.0, pos=2)}
byteorder := 'little'
chunkshape := (4369,)
autoindex := True
colindexes := {
"index": Index(6, medium, shuffle, zlib(1)).is_csi=False}
/df/table._v_attrs (AttributeSet), 15 attributes:
[CLASS := 'TABLE',
FIELD_0_FILL := 0,
FIELD_0_NAME := 'index',
FIELD_1_FILL := 0,
FIELD_1_NAME := 'values_block_0',
FIELD_2_FILL := 0.0,
FIELD_2_NAME := 'values_block_1',
NROWS := 28880943,
TITLE := '',
VERSION := '2.7',
index_kind := 'integer',
values_block_0_dtype := 'int64',
values_block_0_kind := ['node_id', 'thread_id', 'handle_id', 'type', 'begin', 'end', 'duration', 'flags', 'unique_id', 'id'],
values_block_1_dtype := 'float64',
values_block_1_kind := ['DSTL_LS_FULL', 'L2_DMISS', 'L3_MISS', 'kernel_type']]
и еще один% prun с обновленными модулями и полным набором данных:
%prun -l 25 %time f3(profile.events)
CPU times: user 1min 14s, sys: 16.2 s, total: 1min 30s
Wall time: 1min 48s
542678 function calls (542650 primitive calls) in 108.678 seconds
Ordered by: internal time
List reduced from 629 to 25 due to restriction
ncalls tottime percall cumtime percall filename:lineno(function)
640 23.633 0.037 23.633 0.037 {method '_append' of 'tables.hdf5extension.Array' objects}
15 20.852 1.390 20.852 1.390 {method '_append_records' of 'tables.tableextension.Table' objects}
406 19.584 0.048 19.584 0.048 {method '_g_write_slice' of 'tables.hdf5extension.Array' objects}
14244 10.591 0.001 10.591 0.001 {method '_g_read_slice' of 'tables.hdf5extension.Array' objects}
458 9.693 0.021 9.693 0.021 {method 'copy' of 'numpy.ndarray' objects}
15 6.350 0.423 30.989 2.066 pytables.py:3498(write_data_chunk)
80 3.496 0.044 3.496 0.044 {tables.indexesextension.keysort}
41 3.335 0.081 3.376 0.082 {method '_fill_col' of 'tables.tableextension.Row' objects}
20 2.551 0.128 2.551 0.128 {method 'ravel' of 'numpy.ndarray' objects}
101 2.449 0.024 2.449 0.024 {method 'astype' of 'numpy.ndarray' objects}
16 1.789 0.112 1.789 0.112 {method '_g_flush' of 'tables.hdf5extension.Leaf' objects}
2 1.728 0.864 1.728 0.864 common.py:197(_isnull_ndarraylike)
41 0.586 0.014 0.842 0.021 index.py:637(final_idx32)
14490 0.292 0.000 0.616 0.000 array.py:368(_interpret_indexing)
2 0.283 0.142 10.267 5.134 index.py:1158(get_neworder)
274 0.251 0.001 0.251 0.001 {method 'reduce' of 'numpy.ufunc' objects}
39 0.174 0.004 19.373 0.497 index.py:1280(reorder_slice)
57857 0.085 0.000 0.085 0.000 {numpy.core.multiarray.empty}
1 0.083 0.083 35.657 35.657 pytables.py:3424(write_data)
1 0.065 0.065 45.338 45.338 pytables.py:3385(write)
14164 0.065 0.000 7.831 0.001 array.py:615(__getitem__)
28570 0.062 0.000 0.108 0.000 utils.py:47(is_idx)
47 0.055 0.001 0.055 0.001 {numpy.core.multiarray.arange}
28570 0.050 0.000 0.090 0.000 leaf.py:397(_process_range)
87797 0.048 0.000 0.048 0.000 {isinstance}