機械学習チームの林田(@chie8842)です。好きなスポーツはテニスとスノボです。
システムは、その当時の最新の技術で作ったとしても必ずレガシー化します。 機械学習システムも他システムと同様、一度デプロイしたら終わりではなく、継続的なメンテナンスが必要です。昨今機械学習は、特に技術の進歩が目覚ましいため、レガシー化するのも早い分野といえます。本稿ではレガシー化した機械学習アプリケーションのメンテナンスと、それに伴うGPU環境からCPU環境への移行によって、大幅にシステムの運用コストを削減した例をご紹介します。
機械学習アプリケーションにおけるコスト課題
クックパッドにおける最初の大きな機械学習プロジェクトである料理きろくがリリースされたのは、2年前のことです。それ以来、様々な機械学習アプリケーションがデプロイされ、現在では大小含めて30を超える機械学習アプリケーションが運用されています。そのうち、10 個のアプリケーションがGPUサーバ上で運用されており、常時数十台のg2.2xlargeインスタンス1が起動している状況でした。
ここで、これらのGPUインスタンスについて、コスト面における問題が2つありました。 1つは実行環境であるEC2インスタンス料金です。GPUインスタンスは他のGPU非搭載インスタンスと比べて単位時間あたりの使用料金が非常に高価です。たくさんのGPUインスタンスが稼働していることで、クックパッド全体のサーバコストを逼迫させているという問題がありました。 2つめはメンテナンスにかかる人的コストです。GPUサーバをアプリケーション環境として利用することで、通常のモニタリングメトリクスに加えてGPUのリソース監視が必要になります。さらに、コンテナからデバイスドライバを使用するための権限設定など、特別な対応が必要となり、将来的にも人的メンテナンスコストがかかるという問題がありました。
機械学習における処理パフォーマンス
機械学習には、データの特徴をよく表すモデルを作る「学習」と呼ばれる過程と、 作ったモデルを使って分類問題を解くなどの「推論」と呼ばれる過程に分けることができます。 例えば、犬と猫の画像を使って、犬と猫を分類するモデルを作るのが「学習」過程、そのモデルを使って犬と猫の画像を分類するのが「推論」の過程です。
クックパッドでは、GPU環境を利用して定期的にモデルを再学習しているアプリケーションはなく、GPU環境のアプリケーションは、全て「推論」に利用していました。
GPUには、並列処理を高速化できるという特徴があります。機械学習の中でも、特にディープラーニングなどのニューラルネットワークを用いる場合に、GPUが用いられることはよく知られていますが、顕著に処理パフォーマンス上効果的なのは、以下の場合です。
- ディープラーニングにおける「学習」処理2
- 非常に高いパフォーマンスが要求される「推論」処理
実はディープラーニングを使った機械学習アプリケーションでも、GPUが必要なケースというのは、意外と限られているのです。 例えば、Applied Machine Learning at Facebook: A Datacenter Infrastructure Perspectiveという論文では、以下のように、機械学習システムを大規模に利用しているFacebookのサービスでさえ、推論にはCPU環境を利用しているということが、語られています。
Facebook currently relies heavily on CPUs for inference, and both CPUs and GPUs for training,
クックパッドでは、モデル学習にGPUを使っていますが、先に書いたとおり、アプリケーションとして実行しているものにおいて、1.に当てはまるものはありません。また、ユーザに対してレスポンシブに応答を返したり、大量のデータを処理したりするといったものはないため、2.についても回避できる可能性が高いということが分かっていました。 そこで、必要なものはアプリケーションのチューニングを行い、GPUアプリケーションを全てCPU環境に移行することにしました。
CPU環境への移し替えに伴うサービス影響確認とパフォーマンス・チューニング
GPUで動作しているアプリケーションをCPU環境に載せ替えるにあたり、まずはCPU環境への移行によるパフォーマンス影響の調査を行いました。 Webシステムにおけるパフォーマンスの重要な指標として、「スループット」と「レスポンスタイム」があります。 スループットとは、一定の時間内に処理できるトランザクション数、レスポンスタイムとは、クライアントからのアクセスに対しての応答速度です。 目標とするスループットやレスポンスタイムが決まっていれば、それらにあわせればよいですが、今回は目標が明確に決まっていないアプリケーションが多い状態でした。そこでまずはGPUを利用した場合とCPUを利用した場合のパフォーマンスの差を測定し、それをもってプロダクトオーナと協議するという方式をとりました。
以下の節では「料理きろく」3という写真に対して料理/非料理判定を行う機械学習アプリケーションの場合を例にとって、パフォーマンス計測とチューニングの進め方を紹介します。 料理きろくの詳細については以下をご覧ください。
チューニング前のパフォーマンス
料理きろくの機械学習部分は、TensorFlow v1.2.1で実装されていました。 GPUインスタンス上で、アプリケーションの推論部分のみをtensorflow-gpuパッケージとtensorflowパッケージ[^3]で動作させた結果、以下のとおり、tensorflow-gpuを利用したほうが約18倍速いことがわかりました。
GPU | CPU | |
---|---|---|
レスポンスタイム | 0.0723秒/枚 | 1.2776秒/枚 |
なお、上記レスポンスタイムは、下記のように100枚の推論の平均値をとっています。
elapsed_times = [] for i in image_list: image = Image.open(i) start = time.time() model.infer(image) # 推論処理 elapsed_time = time.time() - start # 実行時間 elapsed_times.append(elapsed_time) print(mean(elapsed_times)) # 実行時間の平均値の表示
TensorFlowのチューニング
まずはTensorFlowのレイヤーで、レスポンスタイムを縮めるようチューニングしました。 料理きろくのために行ったチューニングの中では以下の2つの項目が効果的でした。
- TensorFlowバージョンアップ
- tf.Sessionを使い回すようにする
順に説明します。 まず1.についてです。TensorFlowは、非常に開発が活発なソフトウェアです。バージョンが上がるごとにTensorFlow自体の高速化も行われているため、単純にTensorFlowのバージョンを上げることで、パフォーマンスが向上することが予想できました。このため、今回は思い切ってTensorFlowのバージョンを1.2.1から最新の1.12.0に上げました。バージョンアップに伴いリファクタリングは必要でしたが、モデル自体はv1.2.1で学習したものをそのまま使うことができました。
次に2.についてです。TensorFlowでは、定義した内容を実行するために、tf.Sessionというセッションが必要です。 このtf.Sessionの起動にはオーバヘッドがあるため、複数の処理ごとに張り直すと、それだけで処理が非常に重くなります。そのため、1つのセッションをできるだけ使い回すように修正しました。
元のコードの書き方
def hoge(): with tf.Session as sess: sess.run() def fuga(): with tf.Session as sess: sess.run()
セッションを使い回す場合の書き方
sess = tf.Session() def hoge(sess): sess.run() def fuga(sess): sess.run() (省略) sess.close()
その結果、元のCPU版と比べてレスポンスタイムが5分の1程度になりました。
GPU | CPU(チューニング前) | CPU(チューニング後) | |
---|---|---|---|
レスポンスタイム | 0.0723秒/枚 | 1.2776秒/枚 | 0.25秒/枚 |
Dockerコンテナリソースのチューニング
TensorFlowレイヤーにおけるチューニングはここまでとして、次に実際本番環境と同等のCPU環境で動作させる場合のアプリケーションとDockerコンテナリソースをチューニングしました。 CPUで動作させる場合の本番環境は、c5.xlargeインスタンスからなるクラスタを利用します。4 そのためc5.xlarge環境で、リソースをモニタリングしながら、アプリケーションを実行します。 今回の検証ではリソースモニタリングツールとしてsarを利用しました。sarを採用した理由は、非常に軽量で扱いやすく、時刻付きのテキスト形式でログを保存しやすいためです。
結果として、Dockerコンテナ上で、ホストのリソース利用制限を行わずにアプリケーションを実行した結果、レスポンスタイムとリソース使用率は以下のようになりました。
レスポンスタイム | CPU使用率 | メモリ使用率 |
---|---|---|
0.474秒/枚 | 93%程度 | 20%程度 |
TensorFlowは、処理が1プロセスであっても、デフォルトで使用可能なCPUすべてを使います。 1プロセスでCPUをほとんど使ってしまうので、1ホスト上で実行するアプリケーションのプロセス数やスレッド数を増やしても、レスポンスタイムが低下してしまい、スループットもそれほどあがらないだろうということがわかります。このため、1ホスト1コンテナ構成で、スループット確保のためには、スケールアウトを行うしかない、という結論となりました。
移行
上記の実験から、GPUからCPUへの移行のパフォーマンス影響としては、以下のとおりとなりました。
- レスポンスタイムがGPUを利用した場合と比べて0.47秒程度増える
- スループットはサーバのスケールアウト(オートスケール)により担保する
この内容でプロダクトオーナーに了解をとり、移行を行いました。 クックパッドでは、Hakoという、Kubernetesライクなコンテナオーケストレーション環境が導入されており、移行自体は、Dockerイメージの更新及びHakoの定義ファイルの更新のみを行えば、スケールアウトも自動で行われ、サーバ作業を行うことなく実行できます。 Hakoについては、詳しくは以下の記事を参照してください。
最初はGPU環境と並行運用し、最終的にCPU環境のみを残すようにして、移行します。
コスト削減の結果
現在移行による並行運用中のアプリケーションもありますが、料理きろくも含めてすべての移行対象アプリケーションをCPU環境で動作しています。
これによるサーバコスト削減効果を算出したところ、EC2利用料金は元の6分の1程度となり、年間1,500万円以上の節約となることがわかりました。
その他の有効なチューニング
今回はチューニングの一例として、料理きろくをCPU環境に移行する内容を紹介しました。 最後に、料理きろくのチューニングではやらなかったけどその他の一般的に有効なチューニング手法を3つだけ紹介します。
モデルアルゴリズムの変更
料理きろくのモデルは、InceptionV3ベースです。モデル更新を行う必要があるため、今回はしませんでしたが、最近では、MobileNetなどの軽量モデルが発展しており、こうしたモデルに置き換えることで、パフォーマンスが向上する可能性が大きいです。
プロセス数/スレッド数の調整
料理きろくでは、TensorFlowの演算によるCPUがボトルネックとなったため実施しませんでしたが、1プロセスでCPU、メモリ、IOといったリソースを十分に使い切れていない場合、WSGI等を利用してプロセス数、スレッド数を増やすことで、1ホストあたりのスループットをあげることができます。
共有メモリの利用
上記において、例えばプロセス数を増やした場合、各プロセスがメモリ上にモデルをロードすることになります。現状クックパッドで運用しているモデルには、それほど巨大なモデルがありませんが、例えば今後以下ブログにおいて紹介したBERTなどの巨大なモデルを利用したいとなったときは、プロセス間で同じ共有メモリ上のモデルを利用するようにアプリケーションを記述することが有効になるかもしれません。
さいごに
機械学習というと、新しい手法を試し、ハイパーパラメータチューニングを行ってモデルの精度を高めることに興味がある人が多いでしょう。しかしながら、機械学習をサービスに活かすためには、こうしたアーキテクチャ面における「チューニング」も重要な仕事であることを分かっていただけたなら嬉しいです!