すべてのCoreを動かす

Yukio Okuda氏(以下、Okuda)あとはCoreをすべて動かす。Parallelで動かすということで、Parallelというオプションをつける。それだけでは無理で、rangeをNumbaが出しているrangeに置き換えてやると、90倍ぐらい速くなります。これは先ほどのもののだいたい4.4倍。すべてCoreをぐるぐる回します。

ということは困るんです。別にマルチCPUバウンドのものが走っていたら、Coreの食い合いになります。そんなことをやると一気にスピードが落ちます。

そしてコントロールしたい時に、その次にnojilということをやると、nojilのファンクションをつくってくれます。そして、自分で今度はスレッディングをコントロールするわけです。

バージョン3.2から出たThreadPoolExecutorというもがありますから、それで書いてみるときれいにリニアに伸びていきます。

CUDAなんですが、要するに同じコードではないので、最初に持ちあげたコンパシビリティという観点から、あまりお話はいたしません。

ただし、書き直したものがありますので、それは付録に上げておきました。それをやると1,000倍ぐらいは速くなります。NumPyの150倍ぐらいのスピードは出るということになります。

ただ、コンパイルに最初だけ1.8秒という時間がかかります。

ポイントは、Nogil関数にしてくれるということなんです。

Machine Learningにおける高速化

次にMachine Learningに移ります。ニューラルネットワークなど、いろいろ呼び方があるんですが、これは数値解析の1分野なわけです。あれにたくさんのパワーがかけられている。それを使わない手はないというところで、TensorFlowと3つのものをレビューしてみました。

3つに共通しているのは、NumPyを加速しているということ。もう1つはCUDAを使いますが、CUDAのKernelはまったく知らなくてもいいです。NumPyがCPUで動くかCUDAで動くか、ただ指定するだけのイメージです。

共通して、Tensorというオブジェクトがあります。しかし、Tensorというライブラリやパッケージがあるわけではありません。各社各様につくっています。

実はドキュメントは非常にPoorなわけです。競争が激しいですから、なにも知らない人がプログラムを組むのではなくて、今日もあとでKerasのなにをパラメータでこれをやればこうできますよ、注意はこうですよと、バージョンが上がるごとに膨大なドキュメントができてくるわけですが、基本的なローレベルなものはどんどんどこかへ隠れてしまいます。

ということで、私はこう思うということに、クエスチョンマークを付けておきました。

TensorFlowにおけるポイント

TensorFlowの一番のポイントは、アクセスレーションするdeviceは非常に豊富だということです。

通常言われているCPUとCUDA、TPUはGoogle専用のものですが、2ヶ月前に手のひらに乗るTPUは出ました。ですから、NVDIAと完全に衝突する状態が年末に出てくると思います。

ROCmというのはAMDです。先月βバージョンが発表されました。そしてIntelのXeon Phiもやるとずっと公式は言っています。

もう1つ、TensorFlowのいいところは、非常に豊富なモジュールがあるところです。リニア計算やFFTなど、そういうパッケージがそろっています。ですから、それが自分に合っていれば一発で速くなる可能性はあります。

PyTorchとCuPy、CuPyというのはChainerの数値解析の部分ですが、これはCUDAでしか高速になりません。ですから、やはり実行モードの差はどうしても出てきます。動くプラットフォーム、それと自分自身のモードです。

TensorFlowにはEagerというモードとGraphというモードがあります。Eagerというのは、ごく当たり前のモードです。PythonでTensorFlowの文があったら、そこで実行がかかります。

逆にGraphは、Pythonはマクロランゲージになっています。現れたらそれはどこかにスタックされています。計算したよといったら、スタックされたものをステッチしていって、GraphをつくってオプティマイズしたりParallelにしたりして実行していくわけです。

PyTorchはCPUで動きますが、遅くなります。これはもう互換性だけと考えたほうが良いです。一応どこで動かすかはパラメータで指定していきます。

彼らもニューラルネットワークGraphとは言っているんですが、それはニューラルネットワークのGraphであって、普通のコンピューティングGraphとは違うと思います。

やはりモジュールが違うので、コードは変更する必要が出てくるわけです。当然そのNumPyのnpをTensorFlowのtfなどに変えることが当然必要です。関数名も違うものがありますから、関数名も変える必要があります。

CuPyに関しては、NumPyとCompatibleですので、それはまったく考える必要はありません。

Machine Learningパッケージの比較

TensorFlowは計算するとフローティングするとかそんなことはしてくれません。エラーで落ちますから、Cと同じようにキャスティングを明確にしていく必要があります。

PyTorchの場合はdeviceを指定していく。そして自分の環境で実行してやれば、これぐらいのスピードは出てきます。

TensorFlowのGraphというのは、この場合には関数としてCUDAに登録するために、シリーズを頭に入力というか、パラメーターはこれですよ、結果はこれを出してくださいというのを指定すると、それは塊として、Graphとして出してくれます。

実はCPUで動かした時に、やっぱり2.4倍ほど速度は上がりますが、その時にプロセスモニターを見てみます。そうすると、8coreが全部フルに動いています。ということは、こいつらはSIMDをやっています。

だけどまったくドキュメントには書いてありません。

一方、PyTorchは普通の状態で1coreしか動かない。そしてもう1つ、TensorFlowのEagerというのは、まだ実力が出ていません。というのは、Eagerが発表されたのは今年の1月です。それはまだβバージョンでした。

βバージョンだとわかるのは、私が1月にやった発表資料がありますから、日本語なんですが、その中のGraphを見てみればわかります。直線ではないんです。途中でステップしてしまうんです。確かにβだった。

ですが、2ヶ月後には正式版にした。そして8月に突然CondaからMKL版が出たわけです。今までバイナリはGoogleしか出ませんでした。

そのスピードを見ると、π計算で20パーセント速くなっています。MKLが効果を出すのはソルバーです。今日もあったGraphとか、ああいうものをかけた時に、一気にスピードが出ているはずなんです。でもこれはなかなか出てこなかった。

このあとお話ししますが、IntelとCondaは特別な関係にあります。だからこういう状態で走るのか、これも最終的にはGoogleにいくのかわかりません。そうしないと、僕はまだ本当のスピードは出していないと思います。

というのは、2週間前にAutoGraphというのが突然GitHubに上がりました。おいすごいのが上がったぜと言うから見たら、おいおいなんだという話になっているわけです。

TensorFlow Graph

TensorFlowのGraphはすごく高度なGraphです。ですから、ここにあるCUDAの部分にあるようなことをやりたいとか、もう1つはメインメモリにあるデータを、CUDAとか別のノードでコンカレントにアクセスしたい。

そういうことをやりたかったら、もうこれしかやる方法はありません。だけど、そのやり方は、Pythonで書いてありません。TensorFlowの中のコントロール文で自分で書く必要があります。それは非常に難解です。ドキュメントもわからない人にわかるようには書かれていません。

ところが、先ほどのAutoGraphというのは、for文、if文など、ある程度限定したもので書かれたものはGraphにしますというのが出てきたわけです。だから、たぶん2ヶ月経つとこれはβではなくて正式版になると思います。

僕はそれを知らない時にこれを書いているので、スピードは速くならないと思いますが、π計算に関してはあまり得意ではありません。1,000倍をCUDAでループしたり、スレッドでParallelにするのをCUDAでParallelにしたりしても、PyTorchのほうが速いです。僕のコーディングが下手なのかもしれないんですが。

Tensorとはなにかということはドキュメントには書いていません(笑)。アクセススレッドが書いてあるだけなんですけど、バックグラウンドの時にお話ししたcopy_in、copy_outの機能は当然あります。

もっと大事なのは、バッファリングとかキャッシングをしているということです。CUDAでシリアルにバーッと流れてきた時に、途中にワーキングなどが出てきます。それはCPUではまったく戻ってきません。全部CUDAの中にいるから、すごいハイパフォーマンスが出るわけです。

その中でif文などどうするかで、この仕組みはどんどん変わってくると思います。

今回、先ほどAutoGraphの話をしましたが、if文をかけた時に高速にしている、そういったものがあります。やっぱりそこに集中してきている。これはもう流れです。

Daskの可能性

次にDaskのお話をします。Daskの中にスレッドの簡単なGraphをつくって計算する機能があります。

私は少しDaskに期待するところがあったんです。というのは、今データベースは大きな変換期にきています。一言で言うと、カラムデータをメモリがいくつあろうが、自由なサイズで表現できる。それはもうできているんです。それがSAPのHANAです。

彼らは社運をかけて、完全にテイクオフした。SAP自体は割と特殊なビジネスモデルです。それがどんどん拡張できる。ニューラルネットワークやマシンラーニングでどうやってリンクするかというのは当然出てきているわけです。

そうしたなか、HadoopはArrowというプロジェクトをつくっています。実際に行動を起こしています。

それというのは、カラムデータを使っている7つのシステムのインメモリのアクセスメソッドを統一しますよと。そこにPandasが入っているわけです。実はPandasというのは内部データの大きな問題を抱えていて、Pandas2などすごく暗雲が立ち込めているんです。

そうしたなか、PyDataのエコシステムをつくっている連中はどうするか、みんな見ているわけです。そうしたら、CondaでDaskを出しますよと。PandasとArrowの中間部分を俺たちは狙いますよというので出てきたのがDaskなんです。

Daskのホームページを見ると、すごいんです。DARPAとNSFがある。そのあとにGordon Moore Foundがあるんです。Gordon MooreはIntelのファウンダーです。モトローラのファウンダーだった人です。半導体業界のドンですよ。

HHMIというのは航空産業王で資産1兆円ですよ(笑)。ナショナルチームですよ、こんなの。だったらDaskでデファクトになる可能性がある。そうしたら、そのDelayを注目しようということになったわけです。

先ほどのTensorFlowの高度な機能は狙いません。Parallelだけやります。それも関数でParallel Pointを定義してちょうだいよと。

例えばπ計算の時に、get_piでアーギュメントはこれですよと。そして出てきた結果をためておいて、それを今度はDelayedで平均値を出してよと。じゃあn/mを計算してちょうだいよというと、Graphをつくっていくわけです。変数名をかけていって。横にあるGraphは、visualizeというメソッドがついてきます。それを呼んでやるとこの図が出てきます。

m=3でget_piを、今までお見せしたものすべてにかけてみました。

一番上はNumPyなんですが、50パーセントスピードが上がります。いろいろ調べていくと、ユニバーサルファンクションというのはすでにNo-GILで書かれているそうです。

今回のサンプルの場合、3つの関数だけで、動いている時間はすごく短いわけです。この短い時間だけParallelに動くから、50パーセントぐらいしかいかない。

2つすでにお話してあるものは、2.何倍とかになります。これも前にお話しした時には3倍になっているのでオーバーヘッドが大きい。

もう1つ面白いのは、CuPyを見た時に、どうも変化が起きている。どうもNo-GILがからんでいるねと。ほかのものに関してはほとんど変化がありません。

オーバーヘッドなんですが、Delayedを見た時にlaunchのところがどんどん上がっているわけです。

傾きもデータ量が多いとオーバーヘッドが大きくなるようなオーバーヘッドになっています。

一方、ThreadPoolはlaunchはほとんど変わらない。正比例の感じでどんどん伸びていくということで、πの計算においてはどうもDelayedは使わないほうがいいのがわかります。

Summaryです。

私はGILに対するDaskからのガイドというか、なにか出るかと思っていたわけです。ないしはそれを解決するようなものがついてくるかと。まったくGILについてはなにも書いてありません。

レファレンスマニュアルはすでに600ページになっています。出てきているのはただ1つ、インプレースアドとか、これはやってはいけませんよとは書いてあるんですが、なぜかというのはどこにも書いてないんです。

少しググればこれがおかしくなるというのは何ヶ所も報告されています。だから僕は最初のあのプログラムをつくったわけです。

やっぱりCuPyのNogilのもの。あとPyTorchはフリーズして全然動きません。TensorFlowの場合はCPUがセグメンテーションフォールトを起こします。私はPythonを書いていてセグメンテーションフォールトを起こしたのはこれが最初です(笑)。

ThreadingとGIL-Safeは予測できるか

最後にThreadingとGIL-Safeは本当に予測できるのというところをお話ししたいと思います。NumPyとCuPyを同じようにスレッディングした時に、NumPyはほとんど上がらないんです。

これはもっともなんです。3つの関数が動いている時間だけParallelになる。スレッディングした時にそれは等分にして割っているだけで、トータルの実行時間は同じなわけです。だから2倍にしたあとは全然上がらない。

かたや、CuPyは、launchが増えているから、絶対値は悪くなっていますが、傾きは正確に上がってきているわけです。ですからこれはπ計算みたいなループをしないでも、1回の計算でものすごい時間がかかるものをかけた時に、効果はすごく出てくると思います。

もう1つは、計算が速くなってもエラーしていると困るんです。数値計算でよくわからないんですよ。速くなったはいいけど収束しなくなったりとか。

ここでその相対エラーはこういう式になるというのが、Wikipediaにありました。だから6・6でとると、6個というのは6のシグネチャをプロットしていくと、上限が出てくるわけです。

緑は、今回のCuPyの8threadsのものです。上がってきません。

ブルーはわざわざNumPyの先ほどの式の一部を変更してあります。これはインプレースアドになっていません。でも、エラーが起きているわけです。

ではGIL-Safeは予測できるのか? 僕はできないと思います。というのは、2つの関数、とくにCountという非常に簡単な関数。これは2つやって、結果はnとnは一致しなければいけません。Vectorを出して、そのVectorの数を数えただけですから。

そしてそれをスレッドにかけていくと、この黒点になっているところ。これは一致しません。

じゃあ少し上にrandomをつけて、長いやつ、Rng_Countとやると、全然エラーが起きないんです。

もっとおもしろいのは、CountをAtomで動かすと、全然エラーが出ないんです。完全にタイミングで出るか出ないかだけ。もうdeterministicではない。

こういうエラーが起きると、ほかのプログラムでもこんなバグはデバッグできないですよ(笑)。あのマシンでは再現しないし、タイミングで出る、レーシングを起こしているようなことですから。

言えることは、もうNo-GILを使いなさいよということですよ(笑)。いくらNumPyにNo-GILのファンクションがあっても、すべてNo-GILで組まないと何が起こるかわかりませんよということを示していると思います。

なぜGILが必要か?

最後なんですが、じゃあなぜGILが必要かということです。

一番左端にThread-1と書いてあります。これは普通のファンクションと考えてください。スタックエリア、Pythonの場合はフレームなんですが、フレームに変数やNameSpacesが来ます。ですが、すべてのObjectsはHeap Storageに行くんです。Heap Storageの管理というのは、どんなやり方をしても必ずコントロールブロックが出ます。メッシュにしろ、チェーンにしろそれを壊したら大変なことになるんです。

今度はこれをキープしようとしたら、右辺にあろうが左辺にあろうが変数アクセス、Objectsアクセスはすべてミューテックスをかけなければいけない。一気にスピードが落ちます。そこで考えられたのが、ある時間帯だけスレッディングをかければミューテックスをかけなくていい。ミューテックスをかけるのはGILだけだよと。多分こうなっていると思います。

ここまで明確に書いいない。みんな文章になっているのでちんぷんかんぷんなんですが、こう考えるとスッキリします。一番右端、Nogilと言いながら、その中でPythonのコードにアクセスしに行くと、触れちゃいけないところに飛んでいくわけです。そうすると、crashを起こすわけです。

あとは、NogilのNambaの話に戻りますが、すべての関数はサポートされていないのである関数を書き直す必要があります。

これを起こすとこのグラフのところに緑の棒が立っていると思いますが、2/100になるんです。最初に申し上げた「Nambaのindexingは非常に悪いんですよ」というのはこういうことなんです。

実際、これに書き直したものをやっていたら、先ほどのエラーが見つかって、逆算すれば本当にGILってどうなるんだろうというのがはじまった感じなんです。だけど、Nambaの方で1.6倍でParallelsでやってくれます。

きれいなThreadingにはなりません。これはNamba自体のレベルの話だと思うんですが、それでも3倍くらい、トータルで5倍くらいのスピードは出るということになります。

要するに、プログラムを実行したときのランタイムから、3つのことをお話してきました。

理想的なThreadingの加速、それからオンザフライパッケージの比較。そして、Pythonが抱える問題点とその解法ということで、みなさんオンザフライをお使いになるなり、または自分でおやりになるなり参考になればと思います。ということで、終わらせていただきます。