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}

questionAnswers(2)

yourAnswerToTheQuestion