CupyとNumpyのパフォーマンスを比較してみた
GPUで、Numpy互換のAPIで行列計算ができるCupyは活発に更新されています。
sortやinv、最近はsparseまで、numpy(とscipy)の機能の多くをカバーするようになってきて、numpyの代用になりえるものになってきたと思います。
そこでどれだけの機能がサポートされているのか、そして、GPUで計算することにより、どれだけ、速くなるのか?調べてみました。
サポートされているAPI数の比較
ざっくりとした調べ方になりますが、inspect.getmembers()でcupyとnumpyのメンバ数を調べてみます。
>>> import inspect
>>> import cupy
>>> import numpy
>>> len([mem for mem in inspect.getmembers(numpy) if inspect.isfunction(mem[1])])
269
>>> len([mem for mem in inspect.getmembers(cupy) if inspect.isfunction(mem[1])])
121
ということで、numpy.***のメンバは269存在するのに対し、cupy.***のメンバは121存在するので、だいたい半分くらいの機能がサポートされている、ということになります。
cupyがまだサポートしていないnumpyのAPIは
>>> numpy_functions = [mem[0] for mem in inspect.getmembers(numpy) if inspect.isfunction(mem[1])]
>>> cupy_functions = [mem[0] for mem in inspect.getmembers(cupy) if inspect.isfunction(mem[1])]
>>> only_numpy = [function for function in numpy_functions if function not in cupy_functions]
>>> print(only_numpy)
でリストアップすることができます。(長いので記事には書きませんが。)
計算時間の比較条件
続きまして、肝心の計算時間を比較してみます。
計測条件
OS: Ubuntu 16.04
CPU: Intel(R) Core(TM) i7 CPU 920 @ 2.67GHz
CPUメモリ: 12GB
Python: 3.5.3
Numpy 1.13.3 (OpenBLAS)
Cupy 4.0.0b1 (CUDA 8.0)
cupyは最初に使う関数はコンパイルされて計算時間が長くなりますが、以後はコンパイル結果がキャッシュされるので、キャッシュ入手後の状態で計測しています。
CUDAが微妙に古いですが、 諸事情で仕方なし。CUDA9使えばよりCupy有利な計算時間じなります。
numpyの計算時間はtimeモジュールを使って計測します。
cupyはcupy.cudaを使って計測する必要があります。
timeとは異なり、 cupy.cuda.get_elapsed_time(start, end)の出力はmsecであることに注意。
計測スクリプトの全文はNumpy VS. Cupy · GitHubに置きました。
計算時間の比較結果
基本的にGPUは並列して計算ができればできるほど(CPUと相対的には、)速く動作します。
ですので、行列の要素数を変えながら計算時間調べました。
elementwise系
add | ||
Array size | Numpy | Cupy |
100 | 0.001142025 | 0.0580332804 |
10000 | 0.0073647499 | 0.0542643213 |
1000000 | 1.8541741371 | 0.0689798403 |
10000000 | 22.4229431152 | 0.7049964905 |
sub | ||
Array size | Numpy | Cupy |
100 | 0.0010585785 | 0.0562422419 |
10000 | 0.0072026253 | 0.0496534395 |
1000000 | 1.7659640312 | 0.063638401 |
10000000 | 21.4246702194 | 0.6445593262 |
sin | ||
Array size | Numpy | Cupy |
100 | 0.0036048889 | 0.0632198381 |
10000 | 0.2651715279 | 0.0551119995 |
1000000 | 25.3970527649 | 0.1261561584 |
さすがにElementwiseな計算は得意そうです。10000要素以上の行列では圧倒的にcupyが速い。sinがやたら速い。
reduce系
sum | ||
Array size | Numpy | Cupy |
100 | 0.0047969818 | 0.0781324816 |
10000 | 0.0136995316 | 0.0716195202 |
1000000 | 0.7352280617 | 0.5519955063 |
10000000 | 7.388613224 | 5.5220483398 |
argmax | ||
Array size | Numpy | Cupy |
100 | 0.0009727478 | 0.0792086411 |
10000 | 0.0113844872 | 0.0711791992 |
1000000 | 1.1310005188 | 0.7271334076 |
10000000 | 11.6488671303 |
7.236550293 |
さすがにreduce演算は苦手そうです。これらの演算が主体の計算はcupyではあまり速くならないでしょう。
行列積系
tensordot | ||
Array size | Numpy | Cupy |
100 | 0.0173902512 | 0.1158998394 |
10000 | 0.2496337891 | 1.7215263367 |
1000000 | 1387.7195715904 | 74.6218798828 |
matmul | ||
Array size | Numpy | Cupy |
100 | 0.0046825409 | 0.4738044739 |
10000 | 0.4622888565 | 0.4494166565 |
1000000 | 43.664329052 | 12.9769299316 |
einsum | ||
Array size | Numpy | Cupy |
100 | 0.0066685677 | 0.7908892822 |
10000 | 0.192565918 | 1.0777776337 |
1000000 | 31.0439896584 | 2.9332293701 |
速くなるのは1000000要素を越えてからのようです。einsumは色々できるのですが、今回は'...i,...j->...ij'の計算をしました。
残念ながら場合によってはnumpyよりも遅くなる演算もあります。
その他
transpose | ||
Array size | Numpy | Cupy |
100 | 0.0105547905 | 0.0067788798 |
10000 | 0.0125741959 | 0.0110348797 |
1000000 | 0.0111436844 |
0.0131529605 |
配列のコピーでしか無いので、GPUで劇的に速くなることはないでしょう。しかし、意外と負けてない。
sort | ||
Array size | Numpy | Cupy |
100 | 0.0061321259 | 0.6516329956 |
10000 | 0.1652002335 | 0.2315295982 |
1000000 | 16.7721819878 | 3.2199633789 |
10000000 | 176.5223050118 |
30.7312646484 |
sortとか苦手でしょ?と思ったら意外とcupyやりおる。
eigh | ||
Array size | Numpy | Cupy |
100 | 0.1069545746 | 3.3546682739 |
10000 | 7.7713394165 | 20.8182324219 |
1000000 | 799.6927380562 | 859.2246875 |
Array inv | ||
Array size | Numpy | Cupy |
100 | 0.3961801529 | 1.0362390137 |
10000 | 1.05489254 | 2.6873406982 |
1000000 | 215.0195193291 | 88.9207128906 |
eighは速くはならず、invはちょい速い程度。
というわけで、巨大行列(要素数1000000程度?)の計算にはcupyを使ってみてはいかがでしょうか?