Melhorar os pandas (PyTables?) Desempenho de gravação na tabela HDF5
Eu tenho usado pandas para pesquisa agora por cerca de dois meses com grande efeito. Com um grande número de conjuntos de dados de eventos de rastreamento de tamanho médio, o pandas + PyTables (a interface HDF5) faz um tremendo trabalho ao permitir que eu processe dados heterogêneos usando todas as ferramentas Python que eu conheço e amo.
De modo geral, uso o formato Fixed (antigo "Storer") no PyTables, pois meu fluxo de trabalho é write-once, read-many e muitos dos meus conjuntos de dados são dimensionados para carregar 50-100 deles na memória em um tempo sem desvantagens sérias. (NB: Eu faço muito do meu trabalho em máquinas de classe de servidor Opteron com 128GB + de memória do sistema.)
No entanto, para grandes conjuntos de dados (500 MB e superior), gostaria de poder usar as habilidades de acesso aleatório e consultas mais escalonáveis do formato "Tabelas" do PyTables, para que eu possa executar minhas consultas sem memória e, em seguida carregar o resultado muito menor definido na memória para processamento. O grande obstáculo aqui, no entanto, é o desempenho de gravação. Sim, como eu disse, meu fluxo de trabalho é escrever uma vez, ler muitos, mas os tempos relativos ainda são inaceitáveis.
Como exemplo, recentemente executei uma grande fatoração de Cholesky que levou 3 minutos, 8 segundos (188 segundos) em minha máquina de 48 núcleos. Isso gerou um arquivo de rastreamento de ~ 2,2 GB - o rastreamento é gerado em paralelo com o programa, portanto, não há "tempo de criação de rastreio" adicional.
A conversão inicial do meu arquivo de rastreamento binário no formato pandas / PyTables ocupa um bom tempo, mas em grande parte porque o formato binário é deliberadamente fora de ordem para reduzir o impacto no desempenho do próprio gerador de rastreio. Isso também é irrelevante para a perda de desempenho ao mover do formato Armazenador para o formato Tabela.
Meus testes foram executados inicialmente com pandas 0.12, numpy 1.7.1, PyTables 2.4.0 e numexpr 0.20.1. Minha máquina de 48 núcleos funciona a 2,8 GHz por núcleo, e estou escrevendo para um sistema de arquivos ext3 que provavelmente (mas não certamente) em um SSD.
Eu posso escrever o conjunto de dados inteiro para um arquivo HDF5 formato Storer (resultando tamanho: 3.3 GB) em 7,1 segundos. O mesmo conjunto de dados, gravado no formato Tabela (o tamanho do arquivo resultante também é de 3,3 GB), leva 178,7 segundos para ser gravado.
O código é o seguinte:
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))
e a saída é simplesmente
Fixed format write took 7.1
Table format write took 178.7
Meu conjunto de dados tem 28.880.943 linhas e as colunas são tipos de dados básicos:
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
... então não acho que deva haver problemas específicos de dados com a velocidade de gravação.
Eu também tentei adicionar compressão BLOSC, para descartar quaisquer problemas estranhos de E / S que possam afetar um cenário ou outro, mas a compactação parece diminuir o desempenho de ambos igualmente.
Agora, percebo que a documentação do pandas diz que o formato Storer oferece gravações significativamente mais rápidas e leituras ligeiramente mais rápidas. (Eu experimento as leituras mais rápidas, como uma leitura do formato Storer parece demorar cerca de 2,5 segundos, enquanto uma leitura do formato de tabela leva cerca de 10 segundos.) Mas parece realmente excessivo que a gravação de formato de tabela deve levar 25 vezes como desde que o formato Storer escreva.
Alguma das pessoas envolvidas com PyTables ou pandas pode explicar as razões arquiteturais (ou de outra forma) por que escrever no formato de consulta (que requer muito poucos dados extras) deve demorar mais de uma ordem de grandeza? E há alguma esperança de melhorar isso no futuro? Eu adoraria participar de um projeto ou de outro, já que meu campo é computação de alto desempenho e vejo um caso de uso significativo para os dois projetos nesse domínio ... mas seria útil obter alguns esclarecimentos sobre as questões envolvidas primeiro, e / ou alguns conselhos sobre como acelerar as coisas daqueles que sabem como o sistema é construído.
EDITAR:
A execução dos testes anteriores com% prun no IPython fornece a seguinte saída de perfil (algo reduzida para legibilidade) para o formato Armazenado / Fixo:
%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 <20>
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}
e o seguinte para o formato Tabelas:
%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 <40>
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)
EDIT 2:
Correndo novamente com uma cópia de pré-lançamento de pandas 0.13 (puxado em 20 de novembro de 2013 às 11:00 EST), os tempos de gravação para o formato Tables melhoram significativamente, mas ainda não se comparam "razoavelmente" às velocidades de gravação do Storer / Fixed formato.
%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 <20>
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)
Percebi durante a execução desses testes que há longos períodos em que a gravação parece "pausar" (o arquivo no disco não está crescendo ativamente), e ainda há também baixo uso da CPU durante alguns desses períodos.
Eu começo a suspeitar que algumas limitações conhecidas do ext3 podem interagir mal com pandas ou PyTables. Ext3 e outros sistemas de arquivos não baseados em extensão às vezes se esforçam para desvincular arquivos grandes prontamente, e desempenho similar do sistema (baixo uso da CPU, mas longos tempos de espera) é aparente mesmo durante um simples 'rm' de um arquivo de 1GB, por exemplo.
Para esclarecer, em cada caso de teste, certifiquei-me de remover o arquivo existente, se houver, antes de iniciar o teste, para não incorrer em nenhuma penalidade de remoção / sobrescrita de arquivo ext3.
No entanto, ao executar novamente esse teste com index = None, o desempenho melhora drasticamente (~ 50s versus ~ 120 na indexação). Assim, parece que ou este processo continua a ser ligado à CPU (meu sistema tem CPUs AMD Opteron Istanbul relativamente antigas rodando a 2.8GHz, embora também tenha 8 soquetes com 6 CPUs centrais em cada um, todos menos um deles, de claro, ficar ocioso durante a gravação), ou que haja algum conflito entre a maneira como PyTables ou pandas tentam manipular / ler / analisar o arquivo quando parcialmente ou totalmente no sistema de arquivos que causa patologicamente mau comportamento de E / S quando a indexação é ocorrendo.
EDIT 3:
@ Os testes sugeridos por Jeff em um conjunto de dados menor (1,3 GB em disco), depois de atualizar o PyTables de 2.4 para 3.0.0, me trouxeram aqui:
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
Na verdade, meu desempenho parece superar o dele em todos os cenários, exceto quando a indexação está ativada (o padrão). No entanto, a indexação ainda parece ser um assassino, e se a maneira que eu estou interpretando a saída detop
els
enquanto executo esses testes, há períodos de tempo em que não há nenhum processamento significativo nem nenhuma gravação de arquivo acontecendo (ou seja, o uso da CPU para o processo Python está próximo de 0 e o tamanho do arquivo permanece constante). Eu só posso supor que estas são leituras de arquivos. Por que leituras de arquivos estariam causando lentidão é difícil para mim entender, como eu posso carregar de forma confiável um arquivo inteiro de 3 ou mais GB deste disco na memória em menos de 3 segundos. Se eles não são leituras de arquivos, então em que o sistema está 'aguardando'? (Ninguém mais está logado na máquina e não há outra atividade no sistema de arquivos.)
Neste ponto, com versões atualizadas dos módulos python relevantes, o desempenho do meu conjunto de dados original é reduzido para as figuras a seguir. De especial interesse são a hora do sistema, que eu suponho que seja pelo menos um limite superior no tempo gasto na execução do IO, e o tempo do Wall, que talvez possa explicar esses períodos misteriosos de nenhuma atividade de gravação / não CPU.
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
No entanto, parece que a indexação causa lentidão significativa no meu caso de uso. Talvez eu deveria tentar limitar os campos indexados em vez de simplesmente executar o caso padrão (o que pode muito bem ser indexação em todos os campos no DataFrame)? Não tenho certeza de como isso pode afetar os tempos de consulta, especialmente nos casos em que uma consulta é selecionada com base em um campo não indexado.
Por solicitação de Jeff, um ptdump do arquivo resultante.
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']]
e outro% de execução com os módulos atualizados e o conjunto de dados completo:
%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 <25>
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}