pandasからNVIDIA RAPIDSに変えると40倍速くなった件 


データ分析にPython pandasを利用していて、計算にかなり時間がかかっていたのですが、GPU計算ライブラリRAPIDSに変えるとかなり高速化できたので、その知見を共有させて頂きます。


# 1. 演算内容 - トレードシステムにおけるバックテスト 
株式会社オークンでは、現在自動トレードシステムを開発しています。 
トレードシステムでは機械学習を用いた投資判断を行うものが多いですが、我々は**人間の本質的な心理、普遍の真理を追求した「演繹的」な投資戦略**によりトレードシステムを構築しており、我々のシステムの大きな特徴です。 
 
開発の主な流れとしては、プロジェクトメンバーであるプロのトレーダーが考案した投資戦略について、投資戦略の評価・改善を行うためにバックテスト(過去の株価データを用いた売買シミュレーション)を行います。
 
投資戦略は20以上存在し、さらにパラメータを最適化するべく、各投資戦略について数十~数百程度のパラメータの組み合わせがあり、合計約1万通りのバックテストを行う必要があります。



# 2. 開発上で困っていたこと - 膨大なデータ量、処理時間への対応

バックテストに用いるデータは、日本株、米国株の全銘柄、過去20年分の日足[^1]データで、4GB程度です。 
バックテストにおける演算の大部分はpandas.DataFrameによるテクニカル指標の算出ですが、データが膨大なので計算に非常に時間がかかっていました。 

Macbookで計算を行っていたこともあり、膨大なパラメータ組み合わせのバックテストを全て行おうとすると、計算だけで1ヶ月近く掛かってしまい、開発上の最大のボトルネックとなっていました。 
pandasの処理工夫による高速化は以前から行っていましたが、CPU計算では計算速度に限界があり、一定以上の高速化は出来ない状態でした。 
 
これ以上手がなく困っていたところですが、どうやらGPUを使うと大容量の計算を高速にできるとのことで、中でも「RAPIDS」というライブラリを利用すると、簡単にGPU計算が可能になるようで、早速調べてみました。 


[^1]: 日足: 金融商品の値動きを表すチャートの一種で、1日の始値、終値、高値、安値の4本値を、ローソクに似た白や黒の棒で表したもの。(出典: https://www.daiwa.jp/glossary/YST1558.html)



# 3. RAPIDSとは - GPU高速化されたPythonライブラリ
![rapids-logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2948711/5e8564fe-dae0-61a4-fb85-584b491f682a.png)


RAPIDSについて簡単に説明させて頂きます。
* NVIDIA社が提供する、cuDF、cuMLなどのオープンソースライブラリ群。 
* ライブラリを使用した計算はGPU上で行われ、高速に計算可能。 
* 既存のPythonライブラリと同じような感覚で利用できる。 
    * 例: cuDFは、pandasと同じような感覚で利用可能、cuMLはscikit-learnと同じような感覚で利用可能。 


弊社ではpandas DataFrameによる計算が大半でしたので、互換性があり導入しやすいのではと感じました。 


# 4. 事前検証ステップ1 - SageMaker Studio Labでの検証
早速RAPIDSを試したかったのですが、GPU検証環境が無い、、と思っていたところ、なんと[SageMaker Studio Lab](https://studiolab.sagemaker.aws/import/github/rapidsai-community/rapids-smsl/blob/main/rapids-smsl.ipynb)にて試すことができるとのこと。
 
SageMaker Studio labは無料で利用でき、メールアドレスの登録のみで利用可能で、まさにRAPIDSを試すにはぴったりの環境です。 

![SageMaker_Studio_Lab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2948711/41f7e73f-4436-8c31-8919-54ba473fb49e.png)


早速利用してみました。 
pandasのGPU版であるcuDFのトレーニングがあったので、こちらを体験。 
Jupiter Notebookによるチュートリアルが用意されており、対話型で簡単にcuDFの動作確認ができました。
 
cuDFの使用感を軽く共有させて頂きます。 
シリーズの作成は以下のようにpandasと同じ使用感で作成できます。 

``` 
import cudf 
s = cudf.Series([1,2,3,None,4]) 
print(s) 

# 0    1
# 1    2
# 2    3
# 3    NaN
# 4    4
# dtype: int64
```

<br>
DataFrameもpandasと同じように、以下で作成できます
 
``` 
df = cudf.DataFrame({'a': list(range(10)), 
                 	 'b': list(reversed(range(10))), 
                 	 'c': list(range(10)) 
                	}) 
print(df)

#      a   b   c
# 0    0   9   0
# 1    1   8   1
# 2    2   7   2
# 3    3   6   3
# 4    4   5   4
# 5    5   4   5
# 6    6   3   6
# 7    7   2   7
# 8    8   1   8
# 9    9   0   9
``` 
pandasと同じ感覚でcuDFを利用できることを確認。GPU計算と聞くとかなり難しい印象があったのですが、**こんな簡単にGPU処理ができる**とは驚きでした。 
 
cudf以外のRAPIDSライブラリのトレーニングもありましたので、ご興味ある方は是非お試しください！ 


# 5. 事前検証ステップ2 - Launch Padでの検証


RAPIDSについてより詳しく理解したいと思い、続いてNVIDIA社の[Launch Pad](https://www.nvidia.com/ja-jp/data-center/launchpad/)プログラムを利用しました。 
 
Launch Padプログラムとは実務に沿ったテーマについてRAPIDSをはじめとする解析ツールを無償で体験できるプログラムです。動作環境も用意されているので、GPU環境を持ち合わせていなくても利用可能です。 

![LaunchPad_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2948711/82b3bee9-a601-9119-ffa6-971a3a4be595.png)


体験プログラムは多くのテーマから選べるのですが、
今回は弊社システムと扱うデータ内容が似てそうな「データ処理を加速し、価格を予測する AI モデルをトレーニングする」を選択しました。 
 
 
このテーマは、dask_cudfを使ったマルチGPUでの分散DataFrameを用いて、2GB超のデータを読み込み、通常時間のかかる機械学習の前処理を高速に処理し、XGBoostでタクシーの運賃予測を行うという内容です。

<br>

早速試してみました！
<br>


最初の2GB超のデータ読み込みについて、弊社バックテストでも同程度のサイズのデータをpandas.read_csvで読み込みますが、読み込むだけで1,2分かかってしまいます。
しかしdask_cudf.read_csvだと、**読み込みが一瞬で完了**しました。 
 
また、後続の機械学習の前処理ですが、読み込んだタクシー乗車データ(乗車下車位置、運賃等)についてカラムの型変換、列同士の数値計算、一部行のみの抽出、、等、
pandasでは時間がかかる大容量データの処理もdask_cudfを扱うことで**高速に処理**することができました。

トレーニングで扱ったデータは価格情報が含まれており、計算処理も弊社バックテストと少し似ていたので、実際の実務に導入する際のイメージが湧きました。 
<br>


以下Launch Padを利用した所感をまとめます。 


* 動作環境には高スペックなデータセンターGPU A30が使われており、実機を導入する際の参考にできた。 
* 自分の興味のあるテーマ、業務と関連のあるテーマを選ぶことができる。自分の専門に近いテーマで体験できるので、RAPIDSを業務に実務で導入する際のイメージが湧きやすい 
* GPU環境の用意が不要で、すぐにRAPIDSの利用感、GPU計算の速さを体感できる。 
* コードはRAPIDSの基本的なメソッドのみで構成。RAPIDSを使うだけで高速計算が実現できることを体感できる。 
 


# 6. 本検証 - Dell TechnologiesカスタマーソリューションセンターのPOC環境での検証
SageMaker Studio Lab、Launch PadプログラムでRAPIDSの有用性を把握できたので、実際にGPUサーバーを導入したいと考えました。しかし、**GPUサーバーは非常に高額**ですので、RAPIDSが我々のシステム要件に満たしているか、RAPIDSを使うことでバックテストが本当に高速化できるのかを購入前に事前に検証したいと思いました。 
 
すると、[Dell Technologiesカスタマーソリューションセンター様](https://japancatalog.dell.com/c/isg_csc/)がご用意しているPOC環境で、実際に我々のプログラムを実行できるということで、利用させて頂きました。 
 
利用させて頂いたサーバーはPowerEdge XE8545です。
GPUは NVIDIA A100が4枚、NVLinkで接続されているものでした。最高峰スペックの環境を用意頂き、感謝しかありません。 
<br>
早速バックテストでRAPIDSを使うよう検証を始めます。 
SageMaker Studio Lab や Launch Padでは既に環境構築が済んでおりコードを実行するのみでしたが、今回はRAPIDSのインストールから行う必要があります。 
 
が、RAPIDSはDocker Imageが公開されているので、Pullするのみで簡単にインストールが出来ました。 
[こちら](https://rapids.ai/start.html)を参考に、以下のイメージを利用しました
(※ RAPIDSのバージョンについて、検証当時(2022.08)では22.06がStableでしたが、2ヶ月おきに最新バージョンがリリースされているようです)

```
FROM rapidsai/rapidsai-core:22.06-cuda11.5-base-ubuntu20.04-py3.9 
```


<br>
いざコードの書き換え！！
<br><br>


RAPIDSがインストール出来たところで、コード上でpandasを使用している箇所をcuDFに換えて行きます。 
そして、早速バックテストを実行。 
しかし、**ライブラリ名を置き換えるのみでは、思った高速化は得られませんでした。**  
 
原因として、従来のコードでは、銘柄毎にデータを小分け(1MB程度)にしてループしており、逐次処理となってしまっていたので、GPUを活用しきれていなかったようです。

```
group_by_df = all_df.groupby('sc') 
for sc, df in group_by_df:  # 約4000ループ 
   df['sma'] = df['close'].rolling(200).mean()  # 単純移動平均の算出 
```


以下のようにループを排除し、全銘柄をまとめて処理することで、GPUによる大規模並列計算が可能になりました。  

```
df['sma'] = df[['sc','close']].groupby('sc').rolling(200).mean().reset_index()['close'] 
```
<br>
また、処理上、どうしても2重ループで処理しなければならない箇所があり、こちらもボトルネックになっておりました。<br>


現時点ではcudfはpandasのループ処理にあたるiterrowsをサポートしていないようでしたが、こちらはNVIDIA社が提供しているライブラリCUDAを利用することで2重ループをGPU計算することができました。 
NumbaはCUDAもサポートしており、GPUの利用が可能であるようです。


cuda.jit関数にcudf.Seriesを渡して計算する[例](https://docs.rapids.ai/api/cudf/nightly/user_guide/guide-to-udfs.html#lower-level-control-with-custom-numba-kernels)がありましたので紹介させて頂きます。    

# 7. 結果

上記2点を解消すると、計算時間は以下のようになりました。 
 
ベンチマークとして、同環境でのPandasによる計算時間を載せてあります。 
使用したデータは、米国株の日足データ20年分で、約4GBです。 

|  使用ライブラリ  |  計算時間 [s]  |  備考 |
|       -       |     -    | - |
|    pandas     |  249.29  |  |
|     cudf      |   5.74   | pandasの43.4倍高速化 |

ハードウェア用を気にすることなく、**RAPIDSを利用するのみでGPU計算が可能になり、43.4倍も高速化出来た**ので、大変感激しました。 
<br>

その他少しハマった点がありましたので、軽く共有させていただきます。 
<br>
 
##### 1.pandasの関数は現時点ですべての関数をサポートしているわけではない
cuDFはpandasの主要な機能は網羅されているのですが、一部の細かい機能はまだサポートされておらず、pdをcudfに変えるのみだと、細かいエラーが発生しました。 
 
例えば弊社ではpandas.DataFrameの日付のカラムはdt.date型を使用していたのですが、cudfでは現時点でサポートされていなかったので、別の型に変更する対応を行いました。 
 
このあたりも、**RAPIDSが2ヶ月おきにリリースされているということで、多くの機能が順次サポートされてゆく**ことかと思います！ 
<br>


##### 2. numpyはcupyに置き換えが必要 
numpyはCPU処理なので、こちらはGPU版に相当するcupyに置き換えると少し速くなりました。 
<br>
 
##### 3. 前の結果を参照するループ処理を行うには工夫が必要 
バックテストでは指数平滑平均(EMA)の計算が必要なのですが、EMAは1つ前の要素を参照して、次の要素を計算する必要があります。 
しかし、単にcuDFで計算すると、**すべての要素を並列で処理するため、前要素を参照することが出来ませんでした。** 

前の要素を参照したい場合は、データを何分割かして、分割された範囲でループを回して、前の要素の計算をしてから次の要素を計算する、といった手法が必要のようです。
 
たまたま、EMAを計算している[サンプル](https://github.com/NVIDIA/fsi-samples/blob/main/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/cuindicator/ewm.py)があったので、そちらを参考に計算しました。 
※ 尚、cudf.ewm関数の[PR](https://github.com/rapidsai/cudf/pull/9027)がありましたので、近いうちにマージされることを期待します！ 

# 8. マルチGPUによる計算

1枚のGPUのメモリに乗り切らないデータを扱う場合はdask_cudfによりマルチGPUを使うことで処理できます。 
今回はA100が4枚搭載されたサーバーを使わせて頂いたということもあり、検証してみました。 
先程は4GBのデータでしたが、今回は検証用として16GBのダミーデータを用意します。 
 
dask_cudfも以下のコードのみでマルチGPUによる読み込みが出来ました。 

```
import dask_cudf 
from dask_cuda import LocalCUDACluster 
from dask.distributed import Client 
 
cluster = LocalCUDACluster() 
client = Client(cluster) 
ddf = dask_cudf.read_csv(‘...’) 

```
<br>
DataFrameによる計算も殆どcuDFと同じように実行できました。

(dask_cudfでは、persist()やcompute()を実行しなければ実際に計算されないので、注意が必要です。)


<br>
計算結果は以下のようになりました。 

|  使用ライブラリ   |  計算時間 [s]  |  備考 |
| - | - | - |
|  pandas  |  2662   | ファイル読み込みに大半(2000s)程度費やす。  |
|  dask_cudf  |  37.18  |  |

ファイルサイズが大きくなることで、pandasでは読み込み時間がかなり増えてしまいました。 
一方で、dask_cudfでは読み込みにも時間かからず、高速に計算することが出来ました。 

# 9. まとめ
今回RAPIDSを試してみて、今まで使っていたpandasと同じ感覚で使えることが分かりました。
また、開発上のボトルネックであった計算時間について40倍以上も高速化することができました。
今後の開発でも是非RAPIDSを活用していきたいと思います！

pandas.DataFrameに限らず、CPUでの計算速度が遅いと感じている方にも是非RAPIDSでの計算を試してみて頂きたいです。