はじめに
SMN a.i lab. Advent Calendar 202412日目です。
Pandas風ライブラリのパフォーマンス比較 で残課題としていたライブラリの一部についても測定したため、Part 2です。
今回は以下のライブラリを比較対象に追加しました。
- Fireducks
- Vaex
- Modin
- Daft
- Ibis
以下のGPU用ライブラリも加えたかったのですが、Colabへのインストールがうまく行かなかったので見送りました。
- NVTabular
- cuStreamz
Pandas風ライブラリたち
Pandas
もっともポピュラーな表形式データ処理ライブラリです。
PyArrow
Apache ArrowのPython用ライブラリです。
それほどPandas風ではありませんが、表形式のデータ型があります。
Arrow自体は列指向のデータフォーマット仕様であり、公式から多くの言語向けにライブラリが配布されています。
PandasからPyArrowの一部の機能を利用することもできます。
PySpark
Apache SparkのPython用ライブラリです。
それほどPandas風ではありませんが、表形式のデータ型があります。
Spark自体は主に分散処理することを想定したソフトウェアですが、ローカルでも使えます。
Polars
Pandasの代替として近年注目されているようです。
Pandasとある程度互換性があるようです。
Dask
Pandasとの互換性の高さを謳っています。
分散処理にも対応しており、Sparkより高速だと主張しています。
Fireducks
NECが長年に渡って磨き上げてきたスーパーコンピューターのエッセンスを注ぎ込んで開発されたそうです。
Pandasと完全な互換性があると主張しています。
Vaex
最高のパフォーマンスのために"zero memory copy policy"と遅延評価を用いているそうです。
Modin
すべてのコアをマルチスレッドで使い切ることで高速化を実現しているそうです。
Pandasと完全な互換性があると主張しています。
エンジンをRay、Dask、MPIから選ぶことができます。
今回はDaskを使いました。
Daft
SQLとPython DataFrame interfacesの双方を「1級市民」として提供し、DuckDBのパフォーマンス、PolarsのPythonic UX、Sparkの拡張性を統合しているそうです。
分散処理もできます。
Ibis
どちらかというとPyArrowやPySparkに近く、分散処理向けのようです。
各種DBMSを含む20以上ものバックエンドを選んで同じAPIで操作することができます。
今回はバックエンドとしてPolarsを使いました。
cuDF
NVIDIA GPU用のライブラリです。
"cuDF pandas Accelerator Mode" を利用するとPandasと完全な互換性があると主張しています。
Notebook
CPU
https://colab.research.google.com/drive/14ap1MT2MWUKhmgp8iu7RrXI3lTmoKopu
GPU
https://colab.research.google.com/drive/1ilbVj6GQZhNfai49qBNWaoZ12YXA7QN8
CPU
環境
Google Colaboratoryを使用します。
CPUの情報
!lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 2
On-line CPU(s) list: 0,1
Vendor ID: GenuineIntel
Model name: Intel(R) Xeon(R) CPU @ 2.20GHz
CPU family: 6
Model: 79
Thread(s) per core: 2
Core(s) per socket: 1
Socket(s): 1
Stepping: 0
BogoMIPS: 4399.99
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 cl
flush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc re
p_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3
fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand
hypervisor lahf_lm abm 3dnowprefetch invpcid_single ssbd ibrs ibpb stibp
fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm rdseed adx sm
ap xsaveopt arat md_clear arch_capabilities
Virtualization features:
Hypervisor vendor: KVM
Virtualization type: full
Caches (sum of all):
L1d: 32 KiB (1 instance)
L1i: 32 KiB (1 instance)
L2: 256 KiB (1 instance)
L3: 55 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0,1
Vulnerabilities:
Gather data sampling: Not affected
Itlb multihit: Not affected
L1tf: Mitigation; PTE Inversion
Mds: Vulnerable; SMT Host state unknown
Meltdown: Vulnerable
Mmio stale data: Vulnerable
Reg file data sampling: Not affected
Retbleed: Vulnerable
Spec rstack overflow: Not affected
Spec store bypass: Vulnerable
Spectre v1: Vulnerable: __user pointer sanitization and usercopy barriers only; no swa
pgs barriers
Spectre v2: Vulnerable; IBPB: disabled; STIBP: disabled; PBRSB-eIBRS: Not affected; BH
I: Vulnerable (Syscall hardening enabled)
Srbds: Not affected
Tsx async abort: Vulnerable
ホストメモリの情報
!free -h
total used free shared buff/cache available
Mem: 12Gi 644Mi 8.1Gi 1.0Mi 3.9Gi 11Gi
Swap: 0B 0B 0B
ライブラリのインストールとimport
!pip install pyarrow pyspark polars dask fireducks vaex modin[all] getdaft ibis-framework
import pandas as pd
import numpy as np
import pyarrow as pa
import pyarrow.compute as pc
import polars as pl
import dask.dataframe as dd
import fireducks.pandas as fd
import vaex
import modin.pandas as mpd
import daft
import ibis
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
データ準備
100列 * 50万行の表形式のデータをランダムに32ビット浮動小数点数で埋め、カテゴリ列A, B, C, Dを追加します。
あまり大きすぎるとMemoryErrorになるので気を付けてください。
np.random.seed(42)
n_rows = 500000
n_cols = 100
# 100列 * 50万行のデータをランダムに作成
# NVIDIA GPUはたぶん32ビット浮動小数点数の方が性能がいいため32ビットで作成
data = np.random.randn(n_rows, n_cols).astype(np.float32)
df = pd.DataFrame(data, columns=[f'col_{i}' for i in range(n_cols)])
# ある列を基準にグループ化するために、適当なカテゴリ列を追加
df['group'] = np.random.choice(['A', 'B', 'C', 'D'], size=n_rows)
問題
group列でグループ化し平均を取るという処理で比較しました。
Jupyterのtimeitマジックコマンドで10ループ * 7回実行して実行時間の平均を比較します。
また、今回はコピーの速度は主眼ではありませんが極端に差がついたのでついでに測りました。
Pandas
Pandas
%%time
# 時間比較用にコピー
df_copied = df.copy()
CPU times: user 595 ms, sys: 40.9 ms, total: 636 ms
Wall time: 879 ms
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean = df.groupby('group').mean()
421 ms ± 185 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
PyArrow
%%time
arrow_table = pa.Table.from_pandas(df)
CPU times: user 2.56 s, sys: 205 ms, total: 2.77 s
Wall time: 1.65 s
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_pa = arrow_table.group_by('group').aggregate(
[
(f'col_{i}', "mean") for i in range(n_cols)
]
)
119 ms ± 31.7 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
3倍以上早くなり、標準偏差も小さくなりました。
Polars
%%time
df_pl = pl.from_pandas(df)
CPU times: user 1.06 s, sys: 232 ms, total: 1.29 s
Wall time: 829 ms
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_pl = df_pl.group_by('group').mean()
84.5 ms ± 18.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Pandasの4倍くらいです。
Dask
%%time
df_dask = dd.from_pandas(df, npartitions=4)
CPU times: user 1.08 s, sys: 71.7 ms, total: 1.15 s
Wall time: 1.17 s
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_dask= df_dask.groupby('group').mean()
66.8 ms ± 3.87 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Pandasの6倍くらいです。
Fireducks
%%time
df_fd = fd.DataFrame(df)
CPU times: user 3.44 ms, sys: 0 ns, total: 3.44 ms
Wall time: 3.82 ms
%%timeit -r 7 -n 10
grouped_mean_fd = df_fd.groupby('group').mean()
165 µs ± 78.8 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
ここで圧倒的な結果が出ました。
Pandasの2550倍くらいです。
Vaex
%%time
df_vaex = vaex.from_pandas(df)
CPU times: user 743 ms, sys: 47 ms, total: 790 ms
Wall time: 726 ms
%%timeit -r 7 -n 10
grouped_mean_vaex = df_vaex.groupby('group', agg='mean')
2.54 s ± 76.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
なんか遅いですね。
Modin
!export MODIN_ENGINE=dask
%%time
df_modin = mpd.DataFrame(df)
CPU times: user 1.35 s, sys: 612 ms, total: 1.96 s
Wall time: 8.84 s
%%timeit -r 7 -n 10
grouped_mean_modin = df_modin.groupby('group').mean()
28.9 ms ± 6.28 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Pandasの10倍くらいです。
Daft
%%time
df_daft = daft.from_pandas(df)
CPU times: user 2.4 s, sys: 172 ms, total: 2.57 s
Wall time: 2.87 s
%%timeit -r 7 -n 10
grouped_mean_daft = df_daft.groupby('group').mean()
10.2 ms ± 1.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Pandasの40倍くらいです。
Ibis
# バックエンドをPolarsに設定
ibis.set_backend("polars")
client = ibis.pandas.connect({'df': df})
%%time
table = client.table('df')
CPU times: user 180 ms, sys: 34.8 ms, total: 215 ms
Wall time: 290 ms
grouped_mean_ibis = table.group_by('group').aggregate([table[col].mean().name(f'{col}_mean') for col in df.columns if col != 'group'])
%%timeit -r 7 -n 10
grouped_mean_ibis.execute()
17.8 s ± 2.42 s per loop (mean ± std. dev. of 7 runs, 10 loops each)
なんか遅いですね。
PySpark
# Sparkセッションを作成
spark = SparkSession.builder.appName("tabular_data_processing_benchmark").getOrCreate()
%%time
df_spark = spark.createDataFrame(df)
CPU times: user 5min 57s, sys: 6.09 s, total: 6min 3s
Wall time: 6min 19s
今回はコピーは主眼ではありませんが、PySparkはなぜかコピーが異様に遅いです。
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_spark = df_spark.groupBy("group").mean()
The slowest run took 5.57 times longer than the fastest. This could mean that an intermediate result is being cached.
122 ms ± 94.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
PyArrowと同じくらいです。
GPU
環境
Google ColaboratoryでランタイムのタイプをT4 GPUに設定して使用します。
GPUの情報
!nvidia-smi
Mon Nov 11 05:09:59 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.104.05 Driver Version: 535.104.05 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 |
| N/A 43C P8 9W / 70W | 0MiB / 15360MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| No running processes found |
+---------------------------------------------------------------------------------------+
CUDA Toolkitのバージョン
!nvcc --version
ライブラリのインストールとimport
!pip install --extra-index-url=https://pypi.nvidia.com cudf-cu12
import pandas as pd
import numpy as np
import cudf
データ準備
CPUと同じです。
問題
CPUと同じです。
cuDF
%%time
df_cudf = cudf.from_pandas(df)
CPU times: user 600 ms, sys: 224 ms, total: 824 ms
Wall time: 1.03 s
%%timeit -r 7 -n 10
# group列でグループ化し平均を取る
grouped_mean_cudf = df_cudf.groupby('group').mean()
71.9 ms ± 5.22 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Pandasの5倍くらいでした。
結果
平均 | 標準偏差 | |
---|---|---|
Pandas | 421 ms | 185 ms |
PyArrow | 119 ms | 31.7 ms |
Polars | 84.5 ms | 18.1 ms |
Dask | 66.8 ms | 3.87 ms |
Fireducks | 165 µs | 78.8 µs |
Vaex | 2.54 s | 76.8 ms |
Modin | 28.9 ms | 6.28 ms |
Daft | 10.2 ms | 1.06 ms |
Ibis | 17.8 s | 2.42 s |
PySpark | 122 ms | 94.8 ms |
cuDF | 71.9 ms | 5.22 ms |
まとめ
今回、FireDucksが圧倒的な性能で首位でした。
Pandasとの互換性も高いようなので、Pandasを高速化したい方は検討してみてはどうでしょうか。
VaexとIbisについては全然性能が出ませんでした。
Ibisはどちらかというと分散処理向けなのでローカルだと性能が出ないのかもしれませんが、Vaexはそうは見えないため原因はよくわかりません。
環境やデータのサイズ、問題によって結果は変わると思うので、実際に導入を検討する場合は自分の求める条件に合わせて測定してみてください。