Ulepsz pandy (PyTables?) Wydajność zapisu tabeli HDF5

Już od dwóch miesięcy korzystam z pand do badań, aby uzyskać wspaniały efekt. Dzięki dużej liczbie zestawów danych zdarzeń śledzenia średniej wielkości pandy + PyTables (interfejs HDF5) mają ogromne zadanie, pozwalając mi przetwarzać heterogeniczne dane przy użyciu wszystkich znanych mi narzędzi Pythona.

Ogólnie rzecz biorąc, używam formatu Fixed (dawniej „Storer”) w PyTables, ponieważ mój przepływ pracy jest jednokrotny do zapisu, wiele do odczytu, a wiele moich zbiorów danych ma takie rozmiary, że mogę załadować 50-100 z nich do pamięci czas bez poważnych wad. (Uwaga: Wiele pracy wykonuję na komputerach klasy Opteron z pamięcią 128 GB +).

Jednak w przypadku dużych zbiorów danych (500 MB i więcej) chciałbym móc korzystać z bardziej skalowalnych możliwości dostępu losowego i zapytań w formacie „Tabele” PyTables, dzięki czemu mogę wykonywać moje zapytania poza pamięcią, a następnie załaduj znacznie mniejszy zestaw wyników do pamięci w celu przetworzenia. Dużą przeszkodą jest jednak wydajność zapisu. Tak, jak powiedziałem, mój przepływ pracy jest jednokrotny, odczytany, ale względne czasy są wciąż niedopuszczalne.

Jako przykład, niedawno uruchomiłem dużą faktoryzację Choleskiego, która zajęła 3 minuty, 8 sekund (188 sekund) na moim 48-rdzeniowym komputerze. To wygenerowało plik śledzenia o ~ 2,2 GB - ślad jest generowany równolegle z programem, więc nie ma dodatkowego „czasu tworzenia śledzenia”.

Początkowa konwersja mojego pliku śledzenia binarnego do formatu pandas / PyTables zajmuje przyzwoity czas, ale w dużej mierze dlatego, że format binarny jest celowo nieczynny w celu zmniejszenia wpływu samego generatora śledzenia na wydajność. Nie ma to również znaczenia dla utraty wydajności podczas przechodzenia z formatu Storer do formatu Table.

Moje testy były początkowo uruchamiane z pandami 0.12, numpy 1.7.1, PyTables 2.4.0 i numexpr 0.20.1. Moja 48-rdzeniowa maszyna działa z częstotliwością 2,8 GHz na rdzeń i piszę do systemu plików ext3, który prawdopodobnie (ale nie na pewno) jest na dysku SSD.

Mogę napisać cały zbiór danych do pliku HDF5 w formacie Storer (w wyniku tego rozmiar pliku: 3,3 GB) w 7,1 sekundy. Ten sam zestaw danych zapisany w formacie tabeli (wynikowy rozmiar pliku to także 3,3 GB) zajmuje 178,7 sekundy.

Kod jest następujący:

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))

a wyjście jest proste

Fixed format write took 7.1
Table format write took 178.7

Mój zestaw danych ma 28 880 943 wiersze, a kolumny to podstawowe typy danych:

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

... więc nie sądzę, aby z szybkością zapisu były jakieś problemy związane z danymi.

Próbowałem też dodać kompresję BLOSC, aby wykluczyć wszelkie dziwne problemy we / wy, które mogą mieć wpływ na jeden lub drugi scenariusz, ale kompresja wydaje się zmniejszać wydajność obu jednakowo.

Teraz zdaję sobie sprawę, że dokumentacja pand mówi, że format Storer oferuje znacznie szybsze zapisy i nieco szybsze odczyty. (Doświadczam szybszych odczytów, ponieważ odczyt formatu Storer wydaje się trwać około 2,5 sekundy, podczas gdy odczyt formatu tabeli zajmuje około 10 sekund.) Ale naprawdę wydaje się, że zapis w formacie tabeli powinien zająć 25 razy więcej niż tak długo, jak zapisuje się format Storer.

Czy którakolwiek z osób zaangażowanych w PyTables lub pandy może wyjaśnić powody architektoniczne (lub inne) powodujące, że pisanie do formatu zapytywalnego (który wymaga bardzo mało dodatkowych danych) powinno trwać o rząd wielkości dłużej? Czy jest jakaś nadzieja na poprawę tego w przyszłości? Chciałbym wskoczyć do jednego lub drugiego projektu, ponieważ moja dziedzina to wysokowydajne obliczenia i widzę znaczący przypadek użycia w obu projektach w tej domenie .... ale byłoby pomocne wyjaśnienie tego problemy dotyczyły najpierw i / lub kilka porad, jak przyspieszyć proces od tych, którzy wiedzą, jak zbudowany jest system.

EDYTOWAĆ:

Uruchomienie poprzednich testów z% prun w IPython daje następujące (nieco zredukowane dla czytelności) dane wyjściowe profilu dla formatu 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 <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}

i następujące dla formatu tabel:

   %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)

EDYCJA 2:

Uruchamiając ponownie z przedpremierową kopią pand 0.13 (wyciągniętą 20 listopada 2013 r. Około 11:00 EST), czasy zapisu dla formatu tabel poprawiają się znacznie, ale nadal nie porównują „rozsądnie” z prędkościami zapisu Storer / Fixed format.

%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)

Zauważyłem podczas uruchamiania tych testów, że istnieją długie okresy, w których pisanie wydaje się „wstrzymywać” (plik na dysku nie jest aktywnie rozwijany), a mimo to w niektórych z tych okresów jest również niskie użycie procesora.

Zaczynam podejrzewać, że niektóre znane ograniczenia ext3 mogą źle współdziałać z pandami lub PyTables. Ext3 i inne systemy plików nieoparte na rozmiarze czasami mają trudności z natychmiastowym odłączeniem dużych plików, a podobna wydajność systemu (niskie użycie procesora, ale długie czasy oczekiwania) jest widoczna nawet na przykład w przypadku zwykłego „rm” pliku 1 GB.

Aby wyjaśnić, w każdym przypadku testowym, upewniłem się, że usunąłem istniejący plik, jeśli taki istnieje, przed rozpoczęciem testu, aby nie ponieść żadnej kary za usunięcie / nadpisanie pliku ext3.

Jednak po ponownym uruchomieniu tego testu z indeksem = Brak, wydajność poprawia się drastycznie (~ 50s vs ~ 120 podczas indeksowania). Wydaje się więc, że albo ten proces jest nadal związany z procesorem (mój system ma stosunkowo stare procesory AMD Opteron Istanbul działające przy 2,8 GHz, ale ma także 8 gniazd z 6 rdzeniowymi procesorami w każdym, z których wszystkie oprócz jednego z oczywiście, siedzieć bezczynnie podczas zapisu), lub że istnieje jakiś konflikt między sposobem, w jaki PyTables lub pand próbuje manipulować / czytać / analizować plik, gdy jest już częściowo lub w całości na systemie plików, który powoduje patologicznie złe zachowanie we / wy, gdy indeksowanie jest występujący.

EDYCJA 3:

Sugerowane testy @ Jeffa na mniejszym zbiorze danych (1,3 GB na dysku), po uaktualnieniu PyTables z 2.4 do 3.0.0, dostałem tutaj:

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

W rzeczywistości moja wydajność wydaje się bić go we wszystkich scenariuszach, z wyjątkiem sytuacji, gdy indeksowanie jest włączone (ustawienie domyślne). Jednak indeksowanie nadal wydaje się być zabójcą i jeśli sposób, w jaki interpretuję dane wyjściowetop ils podczas wykonywania tych testów jest poprawne, pozostają okresy czasu, w których nie ma ani znaczącego przetwarzania, ani żadnego zapisu pliku (tj. użycie procesora w procesie Pythona jest bliskie 0, a rozmiar pliku pozostaje stały). Mogę tylko założyć, że są to odczyty plików. Trudno mi zrozumieć, dlaczego odczytywanie plików powodowałoby spowolnienie, ponieważ mogę niezawodnie załadować cały plik 3+ GB z tego dysku do pamięci w czasie poniżej 3 sekund. Jeśli nie są to odczyty plików, to na czym polega „oczekiwanie” systemu? (Nikt inny nie jest zalogowany do komputera i nie ma innej aktywności systemu plików.)

W tym momencie, dzięki ulepszonym wersjom odpowiednich modułów Pythona, wydajność mojego oryginalnego zestawu danych jest niższa niż poniższe liczby. Szczególnie interesujący jest czas systemowy, który, jak przypuszczam, jest przynajmniej górnym limitem czasu spędzanego na wykonywaniu IO i czasu Wall, co wydaje się być prawdopodobnie przyczyną tych tajemniczych okresów braku aktywności zapisu / braku procesora.

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

Niemniej jednak wydaje się, że indeksowanie powoduje znaczne spowolnienie mojego przypadku użycia. Być może powinienem spróbować ograniczyć indeksowane pola zamiast po prostu wykonywać domyślny przypadek (który może bardzo dobrze być indeksowany na wszystkich polach DataFrame)? Nie jestem pewien, w jaki sposób może to wpłynąć na czasy zapytań, szczególnie w przypadkach, w których zapytanie wybierane jest na podstawie pola niezindeksowanego.

Na prośbę Jeffa ptdump pliku wynikowego.

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']]

i kolejny% prun ze zaktualizowanymi modułami i pełnym zestawem danych:

%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