Source: Preferred Networks BlogImageNetを15分で学習して以来 [1]、Chainerと沢山のGPUを使って深層学習を並列化し、一回の学習に必要な時間を大きく短縮することができるようになりました。その後、ImageNetの学習は深層学習における並列化 ・高速化のデファクト標準ベンチマークとなりました [2]。それと同時に、深層学習の並列化および大規模化は進み、複数GPUどころか複数ノードで学習することは当たり前のこととなりました。深層学習の計算が大規模化し所要時間はどんどん短くなりましたが、一般的にはノードが増えれば増えただけ部分故障の確率は高くなります。また、大規模なクラスタでは個々の分散ジョブをスケールアウトしたりスケールダウンする機能、つまり可塑性をもとにした計算資源のやりくりが運用上重要になってきます。そこでChainerを拡張し、分散深層学習に耐故障性だけでなく可塑性を導入する実験を行いましたので、ここで報告したいと思います。また、実験に利用したライブラリを eChainer として公開しました [3]。 深層学習の計算が大規模化し所要時間はどんどん短くなりましたが、一般的にはノードが増えれば増えただけ部分故障の確率は高くなります。そこで、部分故障があっても計算が継続できるような仕組みがあれば、このような問題に頭を悩ませるようなこともなくなります。 また、ノードが途中で減っても動作継続できるということは、大抵の場合はノードを途中で追加しても動作継続できるということです。これができると、学習を実行中にGPUを追加することで学習を加速することができます。これは、大規模なクラスタ環境ではとてもメリットが大きいことで、例えば、ジョブ終了や失敗などが原因で、クラスタ上の別の場所で余っていた計算資源を動的にジョブに追加することで、ジョブを早く終わらせることができるようになり、クラスタ上のリソース配分の柔軟性が増します。リソース利用効率を上げることができ、同じ計算基盤への投資でより多くのリターン(よいモデル、よい計算結果)を得ることができるようになります。動的に計算資源と追加したり減らしたりできることを、そのシステムに可塑性(Elasticity)があるといいます。 eChainer というプロトタイプ名は、 Elastic Chainer を短縮してつけたものです。 基本設計とシステム構成 Chainerには、分散学習用の内部コンポーネントが含まれており、ChainerMN という名前がついています。これを使ったこれまでの分散深層学習については、弊社鈴木の解説[4] や、その肝となる AllReduce の計算についてはインターンの上野さんの寄稿 [5] をご覧ください。ChainerMNの Communicator [6] [7] は、MPIにおけるCommunicatorをPythonのインターフェースクラスとして表現し、それを実装したものです。ChainerMNにおける分散深層学習は、基本的に全てこのCommunicatorを使っています。Communicator の実装クラス PureNcclCommunicator を通じて NCCLを利用した場合のイメージを図1に示します。NCCLは、NVIDIAが開発しているGPU専用の集団通信ライブラリです[8] 。ChainerMNでは、CuPyのCythonの実装を通じてNCCLを利用します。 図1: Chainerを使った分散深層学習の内部コンポーネントの構成図: PureNcclCommunicator はCuPy を通じてNCCLを呼び出す いくつかあるCommunicatorの実装は、いずれもシステムの部分故障を前提としたものにはなっていません。最も利用されているのは PureNcclCommunicator ですが、その内部で利用しているNCCLが 2.3 まではシステムの部分故障を前提としたものになっていませんでした。例えば ncclAllReduce() の集団通信に参加しているプロセスがひとつでも故障すると、他のプロセスで呼ばれた ncclAllReduce() は永遠にブロックされたままになります。 このとき、Communicatorの内部で起きていることを詳しく解説します。ChainerMNは内部的にはCuPy経由で ncclAllReduce() を利用します。CuPy は Cython 経由で全ての関数を呼び出します。ところがPythonのC拡張の仕様では、ユーザーから送られるUnixシグナルはPythonが設定したシグナルハンドラによって擬似的にブロックされた状態になり、シグナルに対するコールバックはPython上の実行に戻ってから呼ばれます [9] 。つまり、ncclAllReduce() の呼び出しが戻らない限りは SIGTERM等を全く受け付けず、ずっと ncclAllReduce() の中で止まったままになります。集団通信に参加しているプロセスが一つでも故障すると、その集団通信はずっと終わらないままブロックするということになります。この様子を図 2 に示します。 図2: AllReduce を呼んでいる最中はSIGTERMが擬似的にブロックされ、 ncclAllReduce() 中のループから抜け出すまでユーザー側で処理をすることはできない。ncclAllReduce() は故障プロセスを含む他のプロセスからの通信を待ち続ける。 実用的には、集団通信がハングしていることを検知して、シグナルハンドラを設定できないSIGKILL等で各ノードでひとつひとつ手動でkillしてまわらなければならなくなります。 OpenMPIはこの状態は検知してジョブを強制終了するといったことはできませんから、ジョブが進行しないまま半永久的に計算リソースを占拠し続けることになります。これが、 ncclDestroy() のAPI導入前の状態でした。 ちょうどよいタイミングで、昨年の今頃のことでしたが、NCCL 2.4から導入される ncclDestroy() というAPIをリリース前にテストすることができました [10]。この関数は、NCCLを使ったアプリケーションに耐故障性を導入するために、最小限でありながら十分な機能追加です。個人的なことですが、NCCLの開発者とメールをやり取りしてこのアイディアを聞いたときはに本当に感心しました。わたしが一年近く悩んでいたこの問題について、既存のコードに影響を与えず問題をエレガントに解決するベストなアプローチだったからです。 ChainerMNを利用した分散深層学習が全てCommunicatorに立脚しているということは、これさえ故障に耐えうる仕様になれば、ChainerMN上の分散深層学習は全て耐故障性を備えることができるようになります。ということで、まずはCommunicatorに耐故障性をもたせるためにncclDestroy() を導入することを目指します。 ncclDestroy() は、任意のタイミングで特定の ncclCommunicator (混乱しそうですが、これはNCCLで内部的に利用するオブジェクトです)を破壊して、そのタイミングで実行されていたあらゆる集団通信を強制終了させることができます。つまり、 ncclAllReduce() が故障したプロセスからの永久にこないメッセージを待っているときに横から強制終了させることができるということです。これによって、NCCLの集団通信中にブロックされた状態から戻れなくなるという問題が解決できます。あとは、NCCLを利用するアプリケーションが、どのようにシステムとして故障を検出し、 ncclDestroy() によってどのようにncclAllReduce() の強制終了をハンドリングするかという問題になります。 まず、ノードの死活監視のために etcd を導入します [11]。Chainerを使っている学習プロセス内で etcd クライアントを別スレッド上で動作させ、このスレッドが etcd とKeepAliveをします。これによって、システムの基本構成は図3 のようになります。 図3: eChainer を使った学習プロセスの基本構成: Communicator 内にetcd を用いた故障検出器を追加した。 このように死活監視するチャンネルを設けることによって、ひとつのプロセスの故障を全体に通知することができるようになります。具体的には、各プロセスは etcd の特定のprefix上にエフェメラルファイルを作って、それをお互いに監視することで死活監視とします。エフェメラルファイルとは、クライアントとサーバーの双方が正常に動作してKeepAliveが維持されているときにだけ保持されるファイルです。例えばクライアントが故障すると、一定時間後にこのエフェメラルファイルが消滅し、他のプロセスはこのエフェメラルファイルが消滅した通知を受け取ることができます。実はここが分散システムで最も難しい故障検出の問題を解決している部分なのですが、etcdを使うことで複雑な実装も簡単に済ませることができました。 この通知をトリガーにして、全てのプロセスは学習(および集団通信)を一旦停止します。このときに ncclDestroy() を利用します。これによって動作中の ncclAllReduce() と、それを利用していた NCCL のリングは全プロセスで破棄されます。ncclAllReduce() […]
Read full article »
Followers on Owler
12
President & CEO
Toru Nishikawa