前書き
てるみさん (@telmin_orca) の Day 2/3 の記事に触発されました。Xeon Phi 欲を解消させていただきます。あまりスパコンらしくない話題でスパコン AdC を埋めて申し訳ないですが、KNL の (かつての) 1ユーザーとしてお許しを...
この記事は深層学習フレームワークと呼ばれるソフトウェアを高速に処理する競争で、Xeon Phi がなぜ活躍できなかったについてのポエムです。
本文
東京大学と筑波大学 (+富士通) が共同で開発したスパコンシステムである Oakforest-PACS は、2016年11月の TOP500 で6位と華々しいスタートを切ったようです。同時期のより実アプリケーションに近いと言われる HPCG は3位。システムの構成についてはてるみさんほど詳しく解説出来ないので簡単に書きますと、計8208ノードのそれぞれにに Intel® Xeon PhiTM 7250 プロセッサ (開発コード名: Knights Landing 以下 KNL) を1基搭載し、それらが Intel Omni-Path で Full bisection bandwidth で繋がっているようです。(ちなみに Oakforest と略すと筑波側の先生から怒られが発生するらしいです。お金出してるから仕方なし。)
メニーコア向け OS である McKernel が計算ノードで使用できるなど、2020年の Post K. (富岳) システムに向けた取り組みの一環でもあったようです。そのため高速ファイルシステムの開発についても力を入れており、知名度は TOP500 ほど高くないものの IO500 といったファイルシステムの速度を競うランキングでは、同時期に1位を取るなど非常に優れた成績を収めているようです。
このように玄人が使うと非常に優れたスパコンシステムではありましたが、卒論・修論・博論が近い今くらいの季節であっても自分の投げたバッチジョブが即座に実行されました。学生が忙しさ故にジョブをぽんぽん投げる時期であっても Oakforest-PACS は空いていたのです。もちろん大学の情報基盤センターの所有するシステムであっても学生だけが使うわけではありませんが、Xeon (+Tesla-P100) から構成される Reedbush と比較して1ユーザーが体感できるほどに人気の差がありました。理論性能よりもこれまでのコードの継承を重視した 1 システムがなぜこれほど人気がなかったのか、当時から今にかけて流行っている深層学習 (以下 DL) のワークロードを KNL で走らせてみた、を題材にして書いていきます。注意点ですが KNL はその上で CentOS が走るようになっており、DL ワークロードを走らせるというのは KNL 上で直接プロセスを立ち上げるという意味です。
@kaityo256 さんの記事にあるように B/F (Bandwidth per FLOPS) 比の高い計算機は自分の走らせたいプログラムが満足できる速度で走る可能性は高いです。そして KNL は MCDRAM の搭載からわかるようにそのことは念頭に入れながら設計されたはずです。
しかし、これらの議論はデータを処理する装置 (キャッシュメモリを含むので CPU とは言いにくい) が十分高速かつ、処理するデータが十分多いことを暗に仮定し、そのような状況に最適化する戦略でもあります。言い換えるとそのような仮定が成り立たないとき、B/F 比が高いことを活かせずにその最適化の効果は下がります。誤解を恐れずに言うとそのような状況の例として、スクリプト言語 (Python など) のインタプリタを KNL 上で走らせることが挙げられます。Xeon Phi のようなメニーコア CPU ではコア一つ当たりの性能を落として設計するため、複数のコアを活用できないプログラムは同世代の CPU と比較して低速になってしまいます。
とはいえもちろんこれは Xeon Phi に限った話ではなくマルチコアが前提になった他の CPU でも (スケールは違いますが) 同じことが言えます。そのため通常 Python などによる大規模データ処理は、それそのものではなく裏で並列処理を行える外部ライブラリを使います。どのようなライブラリを使うかは業界によりますが、ここでは DL でよく使われる NumPy を例にあげます。2NumPy の基本的な設計としては入力を C 言語の配列様の構造に保持し、API を経由して抽象的な処理内容をその構造に適用することです。Intel MKL が組み込まれたものであれば一部の処理はそちらにで行うということもあるかもしれませんが、ベクトル演算やマルチスレッド等のプリミティブな並列処理と、ユーザーの指定する処理内容の橋渡しをしているのは基本的に NumPy である、ということが重要です。
このおかげでコアの処理速度を活かせるようになるので上述の仮定がある程度正しくなります。ここで「ある程度」という言葉を使ったのはこの橋渡しには改善の余地があるためです。3
上述の「改善の余地」について書く前にスループットとレイテンシについて書きます。B/F の Bandwidth 側はメモリのスループットに関する値ですが、現実のアプリケーションを考える上はレイテンシも考慮に入れる必要があります。この「レイテンシ」というワードも命令の処理速度の文脈で使われたり、メモリ/ディスクへのアクセス速度の文脈で使われたりと色々な意味を内包していますが、ここではメモリのアクセスについて考えます。例えばわんこそばを茹でて運んで食べるまでの状況について想像してみます。そばが処理対象のデータで食べる人がデータ処理です。茹でる人 (DRAM などのメモリとしましょう) は4分で4杯つくり、運ぶ人 (DRAM からデータをコアへ移動する過程) は2分で2杯運び、食べる人 (コア/スレッド) は1分で1杯食べるというケースでは、十分な時間が経った後であれば、毎分そばが食べる人に運ばれることがわかると思います。
この場合は茹でと運びのバンド幅が1杯/分です。そして十分に時間が経つ前、食べる人が注文を始めてから食べ始めるまでにかかる時間は4+2で6分かかります。この時間がレイテンシに相当するものです。ちなみに茹でと運びのバンド幅が一致しない場合は小さい方がボトルネックとされます。また、食べる人がもっと遅ければ無駄なバンド幅ともなります。ではバンド幅を変えずに茹でる人が16分で16杯つくり、運ぶ人が4分で4杯運ぶ場合にはどうなるかというと、レイテンシは16+4で20分になります。数日間に渡って食べ続ける場合にはこの程度のレイテンシの差は相対的に無視できるほど小さいですが、合計16杯食べるのにかかる時間という指標では前者が22分、後者が36分と30%近くの時間差がつくことがわかります。このように処理するデータが十分に大きくない場合 (=コアが本気を出す時間が短い場合) はこのレイテンシが問題となることがあります。この問題をざっくり評価するモデルとして Roofline model というものがあります。(出典: Roofline model)
これは普段は並列処理速度の文脈で使われることが多いですが、各処理装置が並列処理を開始するまでには複数の対象に入力を配る必要が先にあり、最初に配り終えるまでの時間をここで言うレイテンシと考えれば、同様の問題に由来する現象であることがわかるでしょうか。図中の Bandwidth はカタログ値ではなくレイテンシ成分も含んだ実効的なデータ転送速度を指すようです。ではこのレイテンシ問題をどうするかについてですが...これが難しい。KNL だと特に難しい気がします。ざっと思いつく原因 (と対処法/嘆き) について書いてみますが、
- KNL 上で走る CentOS が全コア使用する並列処理に悪影響=>OS の走るコアを0番に指定してそこを使わない
- スレッド生成に時間がかかりすぎる=>予め生成してプールしておく。念の為使う前に適当な仕事をさせて温めておく
- MCDRAM を使うようにプログラムを書き換えるのしんどい=>Cache モード...(敗北) MKL は真面目に MCDRAM を使っているが...
- コア間は2-Dメッシュでつながってるでしょ。ちゃんと意識して=>メインストリームにない仕様をいきなり出されても...それ真面目に使うだけで論文になっちゃいそう
- コンパイラが AVX-512 を使うのに非常に消極的=>割と intrinsics 前提
- ライブラリを動的にリンクすると命令列のフェッチに時間がかかる=>全部静的リンクにしてしまう (本気で言ってる?)
- x86 が動きます=>動くけど、遅い。上述の努力、やるんですか?
などなど、厳しい事情がありました。他にも「何故か」性能が出ないので悩んでいましたが、当時の自分ではそこを詰め切ることは能力的にできませんでした。(今もできるかと言われたら...)
ここで話を本題に戻して、DL は非常に計算力を必要とするワークロードであるので、このレイテンシの問題は顕在化しないのでは?と考えます。そしてCV 業界でよく使われるモデルのベンチマークを簡易的に取ってみると、畳み込みネットワークがそれなりの時間を食っていることは珍しくありません。この計算内容について長い説明はしませんが、非常に単純化して考えると入力x[CI][H][W]
、w[CO][CI][KH][KW]
に対して以下のように出力y[CO][HO][WO]
を計算するものです。4
for co in range(CO):
for ci in range(CI):
for ho in range(HO):
for wo in range(WO):
for kh in range(KH):
for kw in range(KW):
y[co][ho][wo] += x[ci][ho + kh][wo + kw] * w[co][ci][kh][ko]
これをちょっと賢くやるアイデアとして6重ループの一部分をCI
の軸を潰すように行列積に置き換える方法があります。(この際に行列積として処理するために入力画像と重みを変形する必要がありますが、転置のコストは O(N^2) であるのに対して行列積のコストは O(N^3) であるため、処理が十分に大きければ変形のコストはペイします。)この際に KH/KW など明らかに小さいことがわかっている軸は入力行列中で複製して行列積中でまとめて処理してしまいます。そのためこの6重ループは一回の行列積で処理するような実装になるようです。(画像: ResearchGate, 論文: arXiv)
どれほどきれいに GEMM に落とし込めるかはちゃんと計算していないですが、もとの計算回数が 2*CO*CI*HO*WO*KW*KW
なので一辺が N の正方行列の積を考えると、小さく見積もってだいたい N=(CO*CI*HO*WO*KW*KW)^(1/3)
ほどでしょうか。では各パラメータの大きさは具体的にはどれくらいのものなのか、ということを考えます。2015年の登場で DL 界隈では古いですがベースラインとして人気のある ResNet-50 モデルを例に出すと、それなりに計算が必要なレイヤ (Conv2) で CO=CI=64/HO=WO=56/KW=KH=3 ほどなので行列積のサイズ感としては N=500 くらいです。ということは...Xeon と比べても負けるかいい勝負くらいのサイズ感なわけです。(本当はここに単精度行列積のベンチマーク結果を入れたいわけですが、N<2000 をまともに評価しているベンチマークが全く見つからなくて...) 多くの GEMM ユーザーにとっては N>2000 位からが気になるところですが、DL ワークロードではむしろ N<500 が主戦場になるわけなので、単に GEMM を繰り返す実装をしてしまうと KNL にとって苦しい戦いになります。 入力画像のミニバッチ処理について書き忘れていました。一般的にはモデルの入力となる画像は一枚ずつではなく複数枚まとめて入力します。これによってモデルの状態を更新する回数を減らせるだけではなく、各レイヤでの計算量も増えるため計算機側にとっても有利に働きます。この同時に入力する枚数をミニバッチサイズと呼びますが、これは大体256以下ほどのスケールであるため行列積のサイズ感としては最大で N=3000 ほどです。これくらいの規模であれば Xeon よりも有利になりますが、入力を行列に変換する部分などで効率的な実装がされていないければシングルスレッド処理の性能差が足を引っ張るなど、 Xeon Phi にとって圧倒的有利とまでは言えなかったと思います。(追記: 2020/12/07) ここにきてようやく NumPy の「橋渡し」に改善の余地がある、という話に戻りますと、今の NumPy の表現力ではどのように実装したところで最終的にどのように GEMM を呼ぶか、の話に帰着してしまうので本質的に苦しい戦いを強いられてしまいます。
DL ワークロードの重さはそのように実装された各レイヤ処理の直列な反復に由来するため、最適化にはレイヤ間の接続という一段メタな視点からの最適化が求められました。このようにして NumPy (もしくは汎用的な計算ライブラリ) を使った実装は、より DL に最適化されたものに置き換えられていきました。
最後に当時の DL フレームワークが CUDA エコシステムとの融和を第一としていた状況について書きます。ここまで NVIDIA の GPU 側の話をしていませんでしたが、CUDA エコシステムに載ったソフトウェアとして DL に最適化された cuDNN というものがあり、それに対抗するものとして MKL-DNN (現 oneDNN) というものがあります。どちらも C++ API を持ち、DL フレームワークはそのインターフェースに関わらずその API を呼ぶための実装をしています。
しかし、当時は様々なフレームワークが息をしていた時代かつ、GPU の有用性が広く認められ始めた頃でもあったので、cuDNN を積極的に導入して CUDA エコシステムの利用が容易になっていく一方で、MKL-DNN といった無くても動く、GPU ほど頑張る意味がない CPU 用の最適化は後手に回っていました。つまり上述の NumPy による実装のような、メニーコア CPU にとって苦しい実装が広く使われていました。これには次々と生まれる SoTA なモデルに対応するため、ソフトウェア開発労力がそちらに割かれたというのもあります。Intel は Caffe という比較的 C++ をネイティブにサポートするフレームワークに力を入れていたようでしたが、昨今の Keras/PyTorch の優勢をみるに DL 研究者は Python エコシステムを好むようです。5
そのためか CPU+GPU (他のアクセラレータ) の構成が業界全体として受け入れられ、CPU 側にはデータ処理するためのデータ処理である Python が仕事として回り、その間にアクセラレータが本来のデータ処理をやるという構成が一般的になりました。
更に最近では CPU に Python をやらせる時間すら惜しくなってきたようで、モデルの形状を一旦固定した上でほぼ全ての処理をアクセラレータにやらせる流れもあるようです。近いうちにメニーコア CPU に優しい DL ワークロードを作れるようになっているかもしれませんが、その前に Xeon Phi の流れは終わってしまいました。研究者がもう少し C++ を好んで Caffe あたりが覇権をとっていたら状況は違ったかもしれません。Python とレイヤ処理の行ったり来たりは KNL にとって不都合なことが多かった...
まとめ
- 2017年前後 DL フレームワークの開発が盛んな時期に Xeon Phi 上で走らせてみるととんでもなく遅かった
- 遅い理由を追求していくと、マルチコア CPU の難しいところを煮詰めたプロセッサはそれ専用の最適化を実用上必要とした。既存のコードは動くが非実用的だった
- Intel 的には MKL-DNN を普及させたかったが、DL フレームワークの開発者たちは GPU 対応を優先し、結果的に GPU 側に有利かつ界隈から人気のある Python+α の構成がスタンダードとなった
- MKL-DNN の名前違いである DNNL/oneDNN は最近になってそのようなフレームワークに取り入れられ始めたが、そのころには Xeon Phi は終わってしまった
余談
- Xeon Phi (KNL) は当時いろんな人が気になっていたので、Xeon Phi で出た結果はつたないものでも興味を持ってもらえました (KNL の威を借る)
- 国内学会で KNL の話をしたら XLSOFT のおねーさんから KNL 攻略本 を頂いたことがあります。ありがとうございました。攻略できませんでした
- 単精度でだけ 3TFLOPS (理論値の約半分) くらい出す行列積を書いたこともあったけれども、まさか2つの VP のどちらかが働かないことがあったとは... KNL 何もわからない
- この記事では Xeon Phi の苦手なものに Python-based な DL フレームワークを取り上げましたが、Python の使用そのものが問題だと言いたい訳ではないです。最適化というのは人間の時間を節約するためにあることを忘れてはいけない
- スパコン AdC なのに1ノードでの話しか書いてない (もっと言えば1プロセス) ですね... KNL のアーキレベルの話にも踏み込めていない...
- でも当時は Xeon Phi を使いこなすための方法を考えるだけで研究になりました
- Oakforest-PACS の優れたファイルシステムを褒めたいとは思いましたが、Xeon Phi の遅さで思い出から隠蔽されてしまいました
-
https://www.xlsoft.com/jp/products/intel/tech/seminar/knl/materials/JCAHPC_Next_Generation_of_Supercomputer_Oakforest_PACS.pdf ↩
-
最適化がすすんだ今の実装では直接 NumPy を使うことは少ないです ↩
-
今後制約が強くなっていくプロセッサと人間が本当に書きたい処理内容の「橋渡し」は今も熱い研究対象です。コンパイラ屋さんの仕事は未だ数多く、HPC の方々は待ちきれずに自分で橋渡しをしてしまう ↩
-
今どきこれをそのまま実装している DL フレームワークは存在しません。ご安心ください ↩
-
好んでいないかもしれない。が、自分の目的をいち早く達成できたのが Python だった、というのは多かったのではないでしょうか ↩